diff --git a/docs/README.md b/docs/README.md index 68828d8..5b7922b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,58 +1,190 @@ +# 📘 Digital Logbook (Digital Diary) -## Project Setup Instructions - -### Node.js and npm Versions - -This project requires the following versions: - -- **Node.js**: v22.7.0 -- **Docker**: 27.1.2 -- **Postgres**: 16.3 -- **Node Package Manager**: 10.8.2 - -To ensure compatibility, please use these specific versions. - -### Setting Up Your Environment - -Prior to setting up the enviornment, if you are not familiar with Node.js and NPM go through this awsome [blog post](https://medium.com/@oroz.askarov/all-you-need-to-know-about-npm-and-packages-as-a-beginner-b6fcea8b3519). - -1. **Install Node Version Manager (nvm)**: - If you don't already have `nvm` installed, you can install it by following the instructions [here](https://github.com/nvm-sh/nvm#installing-and-updating). -2. **Navigate to the server directory** - ```bash - cd server/ - ``` -3. **Switch to the required Node.js version**: - Once `nvm` is installed, navigate to the project directory and run: - ```bash - nvm use - ``` -4. **Install the environment incase if it's not already available (optional)** - If step 2 prompts you to install the environment then run: - ```bash - nvm install - ``` -5. **Install the correct npm**: - Install node package manager with this specific version: - ```bash - npm install npm@10.8.2 - ``` - -### Installing dependencies - -1. **Install dependencies by running** - ```bash - npm install - ``` - ### Installing database - - To host the database locally we are going to use docker, please make sure that you have docker desktop installed in your PC. - -- Once you do, if you have cloned the repo for the first time then type this command in the root directory of the project +A web application developed with **Next.js (TypeScript)**, **PostgreSQL** (via **Prisma**), and **Docker**. This project is part of the **TRACE T2T Internship Program** to help interns and mentors track evaluations and logbook entries efficiently. + +--- + +## 🚀 Tech Stack + +- **Frontend/Backend**: [Next.js](https://nextjs.org/) (v14.2.5) with TypeScript +- **Database**: PostgreSQL (v16.3) via [Prisma ORM](https://www.prisma.io/) +- **Containerization**: Docker with Compose +- **Other Services**: Redis, pgAdmin + +--- + +## 🔧 Prerequisites + +Ensure you have the following installed: + +- **Node.js**: `v22.7.0` +- **npm**: `v10.8.2` +- **Docker**: `v27.1.2` +- **PostgreSQL**: Installed **locally** +- **Docker Desktop** running + +--- + +## 📁 Project Setup Instructions + +### 1. Clone the Repository + +```bash +git clone https://github.com/tracet2t/Digital-Logbook.git +cd Digital-Logbook +``` + +--- + +### 2. Configure Environment Variables + +Edit a `.env` file in the root directory: + +```env +BASE_URL=http://localhost:3000 + +# Database +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" + +# Authentication +JWT_SECRET="thisismysecretkey" +JWT_EXPIRY="60 min" +ISSUER=http://localhost:3000 +AUDIENCE=http://localhost:3000 + +# Frontend +NEXT_PUBLIC_BASE_URL=http://localhost:3000 + +# Email Config - Replace with your credentials +EMAIL_USER="your-email@gmail.com" +EMAIL_PASS="your-app-password" +``` + +📝 **Note**: Replace email credentials with valid ones. + +--- + +### 3. Install Node and npm (Using `nvm`) + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash +nvm install 22.7.0 +nvm use +npm install -g npm@10.8.2 +``` + +--- + +### 4. Install PostgreSQL Locally + +You must have PostgreSQL running on your machine (outside Docker): + +- Create a `postgres` database +- Use credentials: + - **User**: `postgres` + - **Password**: `postgres` + +--- + +### 5. Prepare for Initial Docker Build + +#### A. Dockerfile + +In `server/Dockerfile`, **comment out** this line: + +```Dockerfile +# RUN npx prisma migrate deploy +``` + +#### B. docker-compose.yml + +**Comment out** the entire `nextjs-app` block: + +```yaml +# nextjs-app: +# container_name: nextjs-app +# build: +# context: ./server +# dockerfile: Dockerfile +# ports: +# - "3000:3000" +# environment: +# NEXT_PUBLIC_POSTGRES_HOST: pgdb +# NEXT_PUBLIC_POSTGRES_USER: postgres +# NEXT_PUBLIC_POSTGRES_PASSWORD: postgres +# NEXT_PUBLIC_REDIS_HOST: redis +# NEXT_PUBLIC_REDIS_PORT: 6379 +# depends_on: +# - postgres +# - redis +# networks: +# - mynetwork +# dns: +# - 8.8.8.8 +# - 1.1.1.1 ``` + +--- + +## 🐳 Start Docker Services + +```bash docker compose up --build ``` -- If you are trying to run the database for development run + +This will run: + +- **pgAdmin**: [http://localhost:8080](http://localhost:8080) +- **PostgreSQL** +- **Redis** + +--- + +## 🧬 Run Prisma Migrations + +In a **new terminal**: + +```bash +cd server/ +npm install +npx prisma migrate dev ``` -docker compose up + +This sets up your DB schema and generates the Prisma client. + +--- + +## ▶️ Run the App Locally + +```bash +cd server/ +npm run dev ``` + +Then open [http://localhost:3000](http://localhost:3000) + +--- + +## 🛠 pgAdmin Setup (Optional) + +- Go to: [http://localhost:8080](http://localhost:8080) +- Login: + - **Email**: `admin@example.com` + - **Password**: `admin` +- Add a server manually: + - **Host**: Use PostgreSQL container IP (get via `docker inspect`) + - **Username**: `postgres` + - **Password**: `postgres` + +--- + +## 👤 Default Mentor Login + +Use the following default login to access the mentor dashboard: + +- **Email**: `mentor1@gmail.com` +- **Password**: `t2tuser` + +This account is created automatically on first migration. + +--- \ No newline at end of file diff --git a/server/.env b/server/.env index f8bad52..6f602ca 100644 --- a/server/.env +++ b/server/.env @@ -8,7 +8,7 @@ BASE_URL=http://localhost:3000 # See the documentation for all the connection string options: https://pris.ly/d/connection-strings JWT_SECRET=hftgry5486 -DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/testdb?schema=public" JWT_SECRET="thisismysecretkey" JWT_EXPIRY="60 min" ISSUER=http://localhost:3000 diff --git a/server/prisma/migrations/20240827030131_init/migration.sql b/server/prisma/migrations/20240827030131_init/migration.sql deleted file mode 100644 index cb60f61..0000000 --- a/server/prisma/migrations/20240827030131_init/migration.sql +++ /dev/null @@ -1,90 +0,0 @@ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('student', 'mentor', 'admin'); - --- CreateEnum -CREATE TYPE "FeedbackStatus" AS ENUM ('approved', 'rejected', 'pending'); - --- CreateTable -CREATE TABLE "users" ( - "id" TEXT NOT NULL, - "email" TEXT NOT NULL, - "passwordHash" TEXT NOT NULL, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "emailConfirmed" BOOLEAN NOT NULL DEFAULT false, - "role" "Role" NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "users_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "activities" ( - "id" TEXT NOT NULL, - "studentId" TEXT NOT NULL, - "date" TIMESTAMP(3) NOT NULL, - "timeSpent" INTEGER NOT NULL, - "notes" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "activities_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "mentorfeedback" ( - "id" TEXT NOT NULL, - "activityId" TEXT NOT NULL, - "mentorId" TEXT NOT NULL, - "status" "FeedbackStatus" NOT NULL, - "feedbackNotes" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "mentorfeedback_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "reports" ( - "id" TEXT NOT NULL, - "mentorId" TEXT NOT NULL, - "studentId" TEXT, - "reportData" JSONB NOT NULL, - "generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "reports_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "mentorship" ( - "id" TEXT NOT NULL, - "mentorId" TEXT NOT NULL, - "studentId" TEXT NOT NULL, - - CONSTRAINT "mentorship_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); - --- AddForeignKey -ALTER TABLE "activities" ADD CONSTRAINT "activities_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "mentorfeedback" ADD CONSTRAINT "mentorfeedback_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "activities"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "mentorfeedback" ADD CONSTRAINT "mentorfeedback_mentorId_fkey" FOREIGN KEY ("mentorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "reports" ADD CONSTRAINT "reports_mentorId_fkey" FOREIGN KEY ("mentorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "reports" ADD CONSTRAINT "reports_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "mentorship" ADD CONSTRAINT "mentorship_mentorId_fkey" FOREIGN KEY ("mentorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "mentorship" ADD CONSTRAINT "mentorship_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20240830100347_add_is_first_timelogin/migration.sql b/server/prisma/migrations/20240830100347_add_is_first_timelogin/migration.sql deleted file mode 100644 index 466960c..0000000 --- a/server/prisma/migrations/20240830100347_add_is_first_timelogin/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "users" ADD COLUMN "isFirstTimeLogin" BOOLEAN NOT NULL DEFAULT true; diff --git a/server/prisma/migrations/20240906130013_update_mentor_activity_relations/migration.sql b/server/prisma/migrations/20240906130013_update_mentor_activity_relations/migration.sql deleted file mode 100644 index 59d022e..0000000 --- a/server/prisma/migrations/20240906130013_update_mentor_activity_relations/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ --- CreateTable -CREATE TABLE "mentoractivities" ( - "id" TEXT NOT NULL, - "mentorId" TEXT NOT NULL, - "date" TIMESTAMP(3) NOT NULL, - "workingHours" INTEGER NOT NULL, - "activities" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "mentoractivities_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "mentoractivities" ADD CONSTRAINT "mentoractivities_mentorId_fkey" FOREIGN KEY ("mentorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20240909051312_removed_student_attribute_from_bulk_report_relation/migration.sql b/server/prisma/migrations/20240909051312_removed_student_attribute_from_bulk_report_relation/migration.sql deleted file mode 100644 index 8b9bc32..0000000 --- a/server/prisma/migrations/20240909051312_removed_student_attribute_from_bulk_report_relation/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `studentId` on the `reports` table. All the data in the column will be lost. - -*/ --- DropForeignKey -ALTER TABLE "reports" DROP CONSTRAINT "reports_studentId_fkey"; - --- AlterTable -ALTER TABLE "reports" DROP COLUMN "studentId"; diff --git a/server/prisma/migrations/20240909065438_status_for_processes/migration.sql b/server/prisma/migrations/20240909065438_status_for_processes/migration.sql deleted file mode 100644 index 4c7b98d..0000000 --- a/server/prisma/migrations/20240909065438_status_for_processes/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- CreateEnum -CREATE TYPE "ProcessStatus" AS ENUM ('pending', 'error', 'completed', 'wip'); - --- AlterTable -ALTER TABLE "reports" ADD COLUMN "status" "ProcessStatus" NOT NULL DEFAULT 'pending'; diff --git a/server/prisma/migrations/20250324103055_redesign/migration.sql b/server/prisma/migrations/20250324103055_redesign/migration.sql new file mode 100644 index 0000000..dd469f1 --- /dev/null +++ b/server/prisma/migrations/20250324103055_redesign/migration.sql @@ -0,0 +1,123 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('STUDENT', 'MENTOR'); + +-- CreateEnum +CREATE TYPE "reviewStatus" AS ENUM ('approved', 'rejected', 'pending'); + +-- CreateTable +CREATE TABLE "User" ( + "userID" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL, + "emailConfirmed" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("userID") +); + +-- CreateTable +CREATE TABLE "Mentor" ( + "UserID" TEXT NOT NULL, + "expertise" TEXT NOT NULL, + + CONSTRAINT "Mentor_pkey" PRIMARY KEY ("UserID") +); + +-- CreateTable +CREATE TABLE "Student" ( + "UserID" TEXT NOT NULL, + "university" TEXT NOT NULL, + "internshipStartDate" TIMESTAMP(3) NOT NULL, + "duration" INTEGER NOT NULL, + "mentorID" TEXT, + "projectID" INTEGER, + + CONSTRAINT "Student_pkey" PRIMARY KEY ("UserID") +); + +-- CreateTable +CREATE TABLE "Project" ( + "projectID" SERIAL NOT NULL, + "projectTitle" TEXT NOT NULL, + "projectDescription" TEXT NOT NULL, + "progress" DOUBLE PRECISION NOT NULL, + "startedAt" TIMESTAMP(3) NOT NULL, + "deadline" TIMESTAMP(3) NOT NULL, + "teamName" TEXT NOT NULL, + "isDeleted" BOOLEAN NOT NULL DEFAULT false, + "mentorID" TEXT, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("projectID") +); + +-- CreateTable +CREATE TABLE "Activity" ( + "activityID" SERIAL NOT NULL, + "role" "Role" NOT NULL, + "timeSpent" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "userID" TEXT NOT NULL, + + CONSTRAINT "Activity_pkey" PRIMARY KEY ("activityID") +); + +-- CreateTable +CREATE TABLE "Review" ( + "reviewID" SERIAL NOT NULL, + "review" TEXT NOT NULL, + "reviewedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "reviewStatus" NOT NULL, + "activityID" INTEGER NOT NULL, + "mentorID" TEXT NOT NULL, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("reviewID") +); + +-- CreateTable +CREATE TABLE "Report" ( + "reportID" SERIAL NOT NULL, + "reportDate" TIMESTAMP(3) NOT NULL, + "generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" TEXT NOT NULL, + "mentorID" TEXT NOT NULL, + + CONSTRAINT "Report_pkey" PRIMARY KEY ("reportID") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Mentor" ADD CONSTRAINT "Mentor_UserID_fkey" FOREIGN KEY ("UserID") REFERENCES "User"("userID") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Student" ADD CONSTRAINT "Student_UserID_fkey" FOREIGN KEY ("UserID") REFERENCES "User"("userID") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Student" ADD CONSTRAINT "Student_mentorID_fkey" FOREIGN KEY ("mentorID") REFERENCES "Mentor"("UserID") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Student" ADD CONSTRAINT "Student_projectID_fkey" FOREIGN KEY ("projectID") REFERENCES "Project"("projectID") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_mentorID_fkey" FOREIGN KEY ("mentorID") REFERENCES "Mentor"("UserID") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("userID") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_activityID_fkey" FOREIGN KEY ("activityID") REFERENCES "Activity"("activityID") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_mentorID_fkey" FOREIGN KEY ("mentorID") REFERENCES "Mentor"("UserID") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Report" ADD CONSTRAINT "Report_mentorID_fkey" FOREIGN KEY ("mentorID") REFERENCES "Mentor"("UserID") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20250408131843_/migration.sql b/server/prisma/migrations/20250408131843_/migration.sql new file mode 100644 index 0000000..459d703 --- /dev/null +++ b/server/prisma/migrations/20250408131843_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `date` on the `Activity` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Activity" DROP COLUMN "date"; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 0e8d5b8..85164f9 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -1,7 +1,6 @@ generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "linux-musl"] - + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl"] } datasource db { @@ -10,103 +9,97 @@ datasource db { } model User { - id String @id @default(uuid()) - email String @unique - passwordHash String - firstName String - lastName String - emailConfirmed Boolean @default(false) - role Role - isFirstTimeLogin Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - activities Activity[] - mentorFeedback MentorFeedback[] - mentorReports Report[] @relation(name: "mentorReports") - mentorRoles Mentorship[] @relation(name: "mentorRole") - studentRoles Mentorship[] @relation(name: "studentRole") - mentorActivities MentorActivity[] @relation(name: "UserMentorActivities") - - @@map("users") + userID String @id @default(uuid()) + firstName String + lastName String + email String @unique + password String + role Role + emailConfirmed Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + mentor Mentor? + student Student? + activities Activity[] } -model Activity { - id String @id @default(uuid()) - studentId String - date DateTime - timeSpent Int - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - student User @relation(fields: [studentId], references: [id]) - feedback MentorFeedback[] - - @@map("activities") +enum Role { + STUDENT + MENTOR } -model MentorFeedback { - id String @id @default(uuid()) - activityId String - mentorId String - status FeedbackStatus - feedbackNotes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - activity Activity @relation(fields: [activityId], references: [id]) - mentor User @relation(fields: [mentorId], references: [id]) - - @@map("mentorfeedback") +model Mentor { + userID String @id @map("UserID") + expertise String + user User @relation(fields: [userID], references: [userID]) + students Student[] + projects Project[] + reports Report[] + review Review[] } -model Report { - id String @id @default(uuid()) - mentorId String - reportData Json - status ProcessStatus @default(pending) - generatedAt DateTime @default(now()) - mentor User @relation(fields: [mentorId], references: [id], name: "mentorReports") - - @@map("reports") +model Student { + userID String @id @map("UserID") + university String + internshipStartDate DateTime + duration Int + mentorID String? + projectID Int? + user User @relation(fields: [userID], references: [userID]) + mentor Mentor? @relation(fields: [mentorID], references: [userID]) + project Project? @relation(fields: [projectID], references: [projectID]) } -model Mentorship { - id String @id @default(uuid()) - mentorId String - studentId String - mentor User @relation(fields: [mentorId], references: [id], name: "mentorRole") - student User @relation(fields: [studentId], references: [id], name: "studentRole") - - @@map("mentorship") +model Project { + projectID Int @id @default(autoincrement()) + projectTitle String + projectDescription String + progress Float + startedAt DateTime + deadline DateTime + teamName String + isDeleted Boolean @default(false) + mentorID String? + mentor Mentor? @relation(fields: [mentorID], references: [userID]) + students Student[] } -model MentorActivity { - id String @id @default(cuid()) - mentorId String - date DateTime - workingHours Int - activities String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - mentor User @relation(fields: [mentorId], references: [id], name: "UserMentorActivities") - - @@map("mentoractivities") +model Activity { + activityID Int @id @default(autoincrement()) + role Role + timeSpent Int + title String + description String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userID String + user User @relation(fields: [userID], references: [userID]) + reviews Review[] } -enum Role { - student - mentor - admin + +model Review { + reviewID Int @id @default(autoincrement()) + review String + reviewedAt DateTime @default(now()) + status reviewStatus + activityID Int + mentorID String + activity Activity @relation(fields: [activityID], references: [activityID]) + mentor Mentor @relation(fields: [mentorID], references: [userID]) } -enum FeedbackStatus { +enum reviewStatus { approved rejected pending } -enum ProcessStatus { - pending - error - completed - wip -} \ No newline at end of file +model Report { + reportID Int @id @default(autoincrement()) + reportDate DateTime + generatedAt DateTime @default(now()) + status String + mentorID String + mentor Mentor @relation(fields: [mentorID], references: [userID]) +} diff --git a/server/prisma/seed.mjs b/server/prisma/seed.mjs index 58d94af..d455d65 100644 --- a/server/prisma/seed.mjs +++ b/server/prisma/seed.mjs @@ -1,57 +1,152 @@ import { PrismaClient } from "@prisma/client"; -import bcrypt from 'bcrypt'; - +import bcrypt from "bcrypt"; const prisma = new PrismaClient(); async function main() { - // Clear existing data in the reverse order of dependencies + // Clear existing data in reverse order of dependencies + await prisma.review.deleteMany(); await prisma.activity.deleteMany(); - await prisma.mentorActivity.deleteMany(); - await prisma.mentorFeedback.deleteMany(); - await prisma.report.deleteMany(); - await prisma.mentorship.deleteMany(); - await prisma.user.deleteMany(); - - // Symmetric key for password hashing + await prisma.report.deleteMany(); + await prisma.project.deleteMany(); + await prisma.student.deleteMany(); + await prisma.mentor.deleteMany(); + await prisma.user.deleteMany(); + + // Password hashing setup const saltRounds = 10; - const defaultPassword = 't2tuser'; + const defaultPassword = "t2tuser"; const hashedPassword = await bcrypt.hash(defaultPassword, saltRounds); - // Create mentors try { - const mentor1 = await prisma.user.create({ - data: { - firstName: 'Sam', - lastName: 'De', - email: 'mentor1@gmail.com', - passwordHash: hashedPassword, - role: 'mentor', - } - }); - - const mentor2 = await prisma.user.create({ - data: { - firstName: 'Jane', - lastName: 'Smith', - email: 'mentor2@gmail.com', - passwordHash: hashedPassword, - role: 'mentor', - } - }); - - - // console.log({ mentor1, mentor2, student1, student2, student3, feedback1, feedback2, report1, report2 }); -} catch (error) { - console.error('Error seeding data:', error); -} -} + // Create mentors + const mentor1 = await prisma.user.create({ + data: { + firstName: "Sam", + lastName: "De", + email: "mentor1@gmail.com", + password: hashedPassword, + role: "MENTOR", + emailConfirmed: true, + mentor: { + create: { + expertise: "Software Engineering", + }, + }, + }, + }); + + const mentor2 = await prisma.user.create({ + data: { + firstName: "Jane", + lastName: "Smith", + email: "mentor2@gmail.com", + password: hashedPassword, + role: "MENTOR", + emailConfirmed: true, + mentor: { + create: { + expertise: "Data Science", + }, + }, + }, + }); + + // Create students + const student1 = await prisma.user.create({ + data: { + firstName: "Alice", + lastName: "Johnson", + email: "student1@gmail.com", + password: hashedPassword, + role: "STUDENT", + emailConfirmed: true, + student: { + create: { + university: "MIT", + internshipStartDate: new Date("2024-06-01"), + duration: 6, + mentorID: mentor1.userID, + }, + }, + }, + }); + + const student2 = await prisma.user.create({ + data: { + firstName: "Bob", + lastName: "Williams", + email: "student2@gmail.com", + password: hashedPassword, + role: "STUDENT", + emailConfirmed: true, + student: { + create: { + university: "Harvard", + internshipStartDate: new Date("2024-07-01"), + duration: 5, + mentorID: mentor2.userID, + }, + }, + }, + }); -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { + // Create a project + const project = await prisma.project.create({ + data: { + projectTitle: "AI Chatbot Development", + projectDescription: "Building a chatbot using NLP techniques.", + progress: 10.0, + startedAt: new Date(), + deadline: new Date("2024-12-01"), + teamName: "AI Innovators", + mentorID: mentor1.userID, + students: { + connect: [{ userID: student1.userID }], + }, + }, + }); + + // Create an activity + const activity = await prisma.activity.create({ + data: { + role: "STUDENT", + timeSpent: 5, + title: "Research on NLP", + description: "Studied various NLP techniques for chatbot development.", + userID: student1.userID, + }, + }); + + // Create a review + const review = await prisma.review.create({ + data: { + review: "Great work! Keep it up.", + reviewedAt: new Date(), + status: "approved", + activityID: activity.activityID, + mentorID: mentor1.userID, + }, + }); + + // Create a report + const report = await prisma.report.create({ + data: { + reportDate: new Date(), + status: "Generated", + mentorID: mentor1.userID, + }, + }); + + console.log("Seeding completed successfully."); + } catch (error) { + console.error("Error seeding data:", error); + } finally { await prisma.$disconnect(); - }); \ No newline at end of file + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/server/src/app/api/activity/route.ts b/server/src/app/api/activity/route.ts index 9b6b17c..bfaf838 100644 --- a/server/src/app/api/activity/route.ts +++ b/server/src/app/api/activity/route.ts @@ -15,13 +15,13 @@ export const GET = async (req: NextRequest) => { const url = new URL(req.url); const date = url.searchParams.get('date'); - const activities = await activityRepository.findByStudentId(userId, date ? new Date(date) : undefined); + const activities = await activityRepository.findByUserId(userId, date ? new Date(date) : undefined); return NextResponse.json(activities); } catch (error) { console.error("Error fetching activities:", error); return NextResponse.json({ message: "Error fetching activities" }, { status: 500 }); } -}; +}; export const POST = async (req: NextRequest) => { try { @@ -37,7 +37,7 @@ export const POST = async (req: NextRequest) => { return NextResponse.json({ message: "Invalid input data" }, { status: 400 }); } - const newActivity = await activityRepository.createActivity(userId, new Date(date), timeSpent, notes); + const newActivity = await activityRepository.createActivity(userId, new Date(date), timeSpent, "Research" , notes); return NextResponse.json(newActivity, { status: 201 }); } catch (error) { console.error("Error creating activity:", error); diff --git a/server/src/app/api/users/route.ts b/server/src/app/api/users/route.ts index 573d4ab..9a7f8cb 100644 --- a/server/src/app/api/users/route.ts +++ b/server/src/app/api/users/route.ts @@ -1,14 +1,12 @@ // src/api/mentorships.ts import { NextRequest, NextResponse } from "next/server"; import getSession from "@/server_actions/getSession"; -import { MentorshipRepository } from "@/repositories/repositories"; -import { Mentorship, User } from '@prisma/client'; +import { MentorRepository } from "@/repositories/repositories"; -const mentorshipRepository = new MentorshipRepository(); +const mentorRepository = new MentorRepository(); export const dynamic = 'force-dynamic'; - export const GET = async (req: NextRequest) => { try { const session = await getSession(); @@ -21,13 +19,13 @@ export const GET = async (req: NextRequest) => { return NextResponse.json({ message: "Mentor ID not found" }, { status: 401 }); } - // Fetch mentorships using the repository - const mentorships = await mentorshipRepository.getMentorshipsByMentorId(mentorId); - - // Ensure type for mentorships and student - const students = mentorships.map((mentorship: Mentorship & { student: User }) => mentorship.student); + // Fetch students associated with the mentor + const mentor = await mentorRepository.getMentorWithStudents(mentorId); + if (!mentor) { + return NextResponse.json({ message: "Mentor not found" }, { status: 404 }); + } - return NextResponse.json(students); + return NextResponse.json(mentor.students); } catch (error) { console.error("Error fetching students:", error); return NextResponse.json({ message: "Error fetching students" }, { status: 500 }); diff --git a/server/src/app/auth/login/route.ts b/server/src/app/auth/login/route.ts index a35d37c..65bdc49 100644 --- a/server/src/app/auth/login/route.ts +++ b/server/src/app/auth/login/route.ts @@ -21,7 +21,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } - const isPasswordValid = await compare(password, user.passwordHash); + const isPasswordValid = await compare(password, user.password); if (!isPasswordValid) { return NextResponse.json({ error: "Invalid password" }, { status: 401 }); } @@ -30,7 +30,7 @@ export async function POST(request: Request) { const algo = 'HS256'; const token = await new jose.SignJWT({ - id: user.id, + id: user.userID, email: user.email, role: user.role.toString(), fname: user.firstName, @@ -46,9 +46,9 @@ export async function POST(request: Request) { let redirectUrl = `${baseUrl}/unauthorized`; - if (user.role === 'student') { + if (user.role === 'STUDENT') { redirectUrl = !user.emailConfirmed ? `${baseUrl}/reset-password` : `${baseUrl}/student`; - } else if (user.role === 'mentor') { + } else if (user.role === 'MENTOR') { redirectUrl = `${baseUrl}/mentor`; } diff --git a/server/src/components/calendar.tsx b/server/src/components/calendar.tsx index 9b326a8..0aa1833 100644 --- a/server/src/components/calendar.tsx +++ b/server/src/components/calendar.tsx @@ -134,7 +134,7 @@ const TaskCalendar: React.FC = ({ selectedUser }) => { const fetchEvents = async () => { let url = `http://localhost:3000/api/activity?studentId=${studentId}`; - if (role === "mentor") { + if (role === "MENTOR") { url = studentId === selectedUser ? `http://localhost:3000/api/mentor?studentId=${selectedUser}` @@ -144,7 +144,7 @@ const TaskCalendar: React.FC = ({ selectedUser }) => { const data = await fetchEventData(url); if (data) { const parsedEvents = - role === "mentor" + role === "MENTOR" ? studentId === selectedUser ?convertToCalendarEventsMentor(data) : convertToCalendarEvents(data): convertToCalendarEvents(data); setEvents(parsedEvents); diff --git a/server/src/components/mentorDashboard.tsx b/server/src/components/mentorDashboard.tsx index 9e74fdf..a4b7bb5 100644 --- a/server/src/components/mentorDashboard.tsx +++ b/server/src/components/mentorDashboard.tsx @@ -1,34 +1,178 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; import Navbar from '@/components/navBar'; import DashboardOverview from '@/components/dashboardOverview'; import ActiveProjects from '@/components/activeProjects'; import PersonalJournal from '@/components/personalJournal'; import Calendar from '@/components/newCalendar'; import Footer from '@/components/footer'; +import Button from '@/components/button'; // Make sure Button component exists +import MentorRegStudentForm from '@/components/mentorRegStudentForm'; // Make sure this exists +import { getSessionOnClient } from '@/utils/session'; // Your session util +import { Session } from '@/types'; // Adjust import based on your types const MentorDashboard = () => { + const [showForm, setShowForm] = useState(false); + const [users, setUsers] = useState<{ userID: string; firstName: string; lastName: string }[]>([]); + const [session, setSession] = useState(null); + const [mentorName, setMentorName] = useState(null); + const [mentorId, setMentorId] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [role, setRole] = useState(null); + const [toast, setToast] = useState<{ title: string; description: string } | null>(null); + const router = useRouter(); + + useEffect(() => { + getSessionOnClient() + .then((data) => { + if (data) { + setSession(data); + setMentorName(`${data.fname} ${data.lname}`); + setMentorId(data.id); + setRole(data.role); + setSelectedUser(data.id); + } + }) + .catch((error) => console.error('Error fetching session:', error)); + }, []); + + const fetchUsers = async () => { + try { + const response = await fetch('http://localhost:3000/api/users'); + const data = await response.json(); + const formattedUsers = data.map((user: any) => ({ + userID: user.userID, + firstName: user.user?.firstName || '', + lastName: user.user?.lastName || '', + })); + setUsers(formattedUsers); + setIsLoading(false); + } catch (error) { + console.error('Failed to fetch users:', error); + setIsLoading(false); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + const handleOpenForm = () => setShowForm(true); + const handleCloseForm = () => setShowForm(false); + const handleMentorChange = (e: React.ChangeEvent) => setSelectedUser(e.target.value); + + const handleReport = async () => { + try { + const response = await fetch(`/api/report?studentId=${selectedUser}`); + if (!response.ok) throw new Error('Failed to generate report'); + + const contentDisposition = response.headers.get('Content-Disposition'); + const filenameMatch = contentDisposition?.match(/filename="(.+)"/); + const filename = filenameMatch ? filenameMatch[1] : 'student_activity_report.csv'; + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + + setToast({ title: 'Report Generated', description: 'Student report downloaded successfully!' }); + setTimeout(() => setToast(null), 3000); + } catch (error) { + console.error('Failed to download report:', error); + setToast({ title: 'Error', description: 'Failed to download the report. Please try again.' }); + setTimeout(() => setToast(null), 3000); + } + }; + + const handleBulkReportClick = async () => { + try { + const response = await fetch('/api/generateReport', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); + if (response.ok) { + const data = await response.json(); + console.log('Report Generation Job ID:', data.jobId); + router.push('/mentor/bulkreport'); + } else { + console.error('Failed to generate bulk report.'); + } + } catch (error) { + console.error('Error generating bulk report:', error); + } + }; + return (
- {/* Navbar */} - + + {/* Top controls */} +
+ {!isLoading && session ? ( + + ) : ( +

Loading...

+ )} + +
+ + + + + + + + {showForm && } +
+
+ {/* Main Content */}
- {/* Left Panel - Dashboard Overview & Active Projects */} + {/* Left Panel */}
-
+
+ +
- {/* Right Panel - Calendar & Journal */} + {/* Right Panel */}
- - {/* Footer */} +
+ + {/* Toast */} + {toast && ( +
+

{toast.title}

+

{toast.description}

+
+ )}
); }; diff --git a/server/src/repositories/repositories.ts b/server/src/repositories/repositories.ts index 4d16366..c873818 100644 --- a/server/src/repositories/repositories.ts +++ b/server/src/repositories/repositories.ts @@ -1,5 +1,5 @@ import prisma from "@/lib/prisma"; -import { User, Activity, Mentorship, MentorActivity, Report, MentorFeedback } from "@prisma/client"; +import { User, Activity, Mentor, Student, Project, Review, Report } from "@prisma/client"; import BaseRepository from "./baseRepository"; /* @@ -7,9 +7,7 @@ import BaseRepository from "./baseRepository"; //-- User Repository --// */ - export class UserRepository extends BaseRepository { - constructor() { super(prisma.user); } @@ -20,11 +18,9 @@ export class UserRepository extends BaseRepository { }); } - async getUserWithActivities(studentId: string) { + async getUserWithActivities(userId: string) { return this.modelClient.findUnique({ - where: { - id: studentId, - }, + where: { userID: userId }, select: { firstName: true, lastName: true, @@ -32,11 +28,12 @@ export class UserRepository extends BaseRepository { select: { date: true, timeSpent: true, - notes: true, - feedback: { + title: true, + description: true, + reviews: { select: { status: true, - feedbackNotes: true, + review: true, }, }, }, @@ -44,7 +41,6 @@ export class UserRepository extends BaseRepository { }, }); } - } /* @@ -52,117 +48,85 @@ export class UserRepository extends BaseRepository { //-- Activity Repository --// */ - export class ActivityRepository extends BaseRepository { - constructor() { super(prisma.activity); } - - async findByStudentId(studentId: string, date?: Date) { + async findByUserId(userId: string, date?: Date) { return this.modelClient.findMany({ where: { - studentId, + userID: userId, ...(date && { date }), }, include: { - feedback: { + reviews: { select: { status: true, - feedbackNotes: true, + review: true, }, }, }, }); } - async createActivity(studentId: string, date: Date, timeSpent: number, notes: string) { + async createActivity(userId: string, date: Date, timeSpent: number, title: string, description: string) { return this.modelClient.create({ data: { - studentId, + userID: userId, date, timeSpent, - notes, + title, + description, }, }); } - async updateActivity(id: string, studentId: string, data: { timeSpent?: number; notes?: string }) { + async updateActivity(id: string, userId: string, data: { timeSpent?: number; title?: string; notes?: string }) { return this.modelClient.update({ where: { - id, - studentId, + activityID: id, + userID: userId, }, data, }); } - async findActivityById(id: string) { - return this.modelClient.findUnique({ - where: { id }, - select: { createdAt: true, studentId: true }, - }); - } - async deleteActivity(id: string) { return this.modelClient.delete({ - where: { id }, + where: { activityID: id }, }); } - async getStudentFeedbacks(studentId: string, date?: string) { - return this.modelClient.findMany({ - where: { - studentId: studentId, - ...(date && { date: new Date(date) }), - }, - include: { - feedback: { - select: { - status: true, - feedbackNotes: true, - }, - }, - }, + async findActivityById(id: string) { + return this.modelClient.findUnique({ + where: { activityID: id }, + select: { createdAt: true, userID: true }, }); } - } /* - //-- Mentorship Repository --// + //-- Mentor Repository --// */ - -export class MentorshipRepository extends BaseRepository { - +export class MentorRepository extends BaseRepository { constructor() { - super(prisma.mentorship); + super(prisma.mentor); } - async getMentorWithStudents(mentorId: string): Promise { - return this.modelClient.findMany({ - where: { mentorId }, + async getMentorWithStudents(mentorId: string) { + return this.modelClient.findUnique({ + where: { userID: mentorId }, include: { - student: { + students: { select: { - id: true, - firstName: true, - lastName: true, - activities: { + userID: true, + user: { select: { - id: true, - date: true, - timeSpent: true, - notes: true, - feedback: { - select: { - status: true, - feedbackNotes: true, - }, - }, + firstName: true, + lastName: true, }, }, }, @@ -172,130 +136,120 @@ export class MentorshipRepository extends BaseRepository { } async getMentorshipsByMentorId(mentorId: string) { - return this.modelClient.findMany({ - where: { mentorId: mentorId }, + return prisma.project.findMany({ + where: { mentorID: mentorId }, include: { - student: true, // Fetch the student details associated with each mentorship + students: { + select: { + userID: true, + user: { + select: { + firstName: true, + lastName: true, + }, + }, + }, + }, }, }); } - } /* - //-- Mentor Activity Repository --// + //-- Student Repository --// */ - - -export class MentorRepository extends BaseRepository { - +export class StudentRepository extends BaseRepository { constructor() { - super(prisma.mentorActivity); - } - - async getMentorActivities(mentorId: string, date?: Date) { - return this.modelClient.findMany({ - where: { - mentorId, - ...(date && { date }), - }, - }); - } - - async createMentorActivity(data: { - mentorId: string; - date: Date; - workingHours: number; - activities: string; - }) { - return this.modelClient.create({ - data, - }); + super(prisma.student); } - async updateMentorActivity(id: string, mentorId: string, data: { - workingHours?: number; - activities?: string; - }) { - return this.modelClient.update({ - where: { - id, - mentorId, + async getStudentWithMentor(studentId: string) { + return this.modelClient.findUnique({ + where: { userID: studentId }, + include: { + mentor: { + select: { + userID: true, + user: { + select: { + firstName: true, + lastName: true, + }, + }, }, - data, - }); + }, + }, + }); } } /* - //-- Report Repository --// + //-- Project Repository --// */ - - -export class ReportRepository extends BaseRepository { - +export class ProjectRepository extends BaseRepository { constructor() { - super(prisma.report); + super(prisma.project); } - -} + async getProjectsByMentorId(mentorId: string) { + return this.modelClient.findMany({ + where: { mentorID: mentorId }, + include: { + students: { + select: { + userID: true, + user: { + select: { + firstName: true, + lastName: true, + }, + }, + }, + }, + }, + }); + } +} /* - //-- Mentor Feedback Repository --// + //-- Review Repository --// */ - - -export class MentorFeedbackRepository extends BaseRepository { - +export class ReviewRepository extends BaseRepository { constructor() { - super(prisma.mentorFeedback); + super(prisma.review); } - async getFeedbackByActivityId(activityId: string, date?: Date) { - return this.modelClient.findFirst({ - where: { - activityId: String(activityId), - activity: { - date: date ? new Date(date) : undefined, - }, - }, + async getReviewsByActivityId(activityId: string) { + return this.modelClient.findMany({ + where: { activityID: activityId }, }); } async upsertFeedback(activityId: string, mentorId: string, review: string, status: string) { - // Check if the feedback already exists const existingFeedback = await this.modelClient.findFirst({ where: { - activityId, - mentorId, + activityID: activityId, + mentorID: mentorId, }, }); if (existingFeedback) { - // Update the existing feedback return this.modelClient.update({ - where: { - id: existingFeedback.id, - }, - data: { - feedbackNotes: review, - status, - }, + where: { id: existingFeedback.id }, + data: { review, status }, }); } else { - // Create new feedback return this.modelClient.create({ data: { - activityId, - mentorId, - feedbackNotes: review, + activityID: activityId, + mentorID: mentorId, + review, status, }, }); @@ -303,4 +257,19 @@ export class MentorFeedbackRepository extends BaseRepository { } } +/* + //-- Report Repository --// + +*/ +export class ReportRepository extends BaseRepository { + constructor() { + super(prisma.report); + } + + async getReportsByMentorId(mentorId: string) { + return this.modelClient.findMany({ + where: { mentorID: mentorId }, + }); + } +}