Browse Source

holy shit

Matthew Trejo 4 months ago
parent
commit
d2f11ebc9d
49 changed files with 9322 additions and 716 deletions
  1. 21 0
      components.json
  2. 10 0
      drizzle.config.ts
  3. 99 0
      drizzle/0000_goofy_obadiah_stane.sql
  4. 12 0
      drizzle/0001_normal_juggernaut.sql
  5. 703 0
      drizzle/meta/0000_snapshot.json
  6. 751 0
      drizzle/meta/0001_snapshot.json
  7. 20 0
      drizzle/meta/_journal.json
  8. 2157 586
      package-lock.json
  9. 26 7
      package.json
  10. 295 0
      src/app/admin/classes/page.tsx
  11. 277 0
      src/app/admin/dashboard/page.tsx
  12. 325 0
      src/app/admin/partials/page.tsx
  13. 340 0
      src/app/admin/periods/page.tsx
  14. 353 0
      src/app/admin/sections/page.tsx
  15. 324 0
      src/app/admin/students/page.tsx
  16. 350 0
      src/app/admin/teachers/page.tsx
  17. 163 0
      src/app/api/admin/classes/[id]/route.ts
  18. 111 0
      src/app/api/admin/classes/route.ts
  19. 181 0
      src/app/api/admin/partials/[id]/route.ts
  20. 133 0
      src/app/api/admin/partials/route.ts
  21. 176 0
      src/app/api/admin/periods/[id]/route.ts
  22. 70 0
      src/app/api/admin/periods/[id]/toggle/route.ts
  23. 110 0
      src/app/api/admin/periods/route.ts
  24. 206 0
      src/app/api/admin/sections/[id]/route.ts
  25. 148 0
      src/app/api/admin/sections/route.ts
  26. 69 0
      src/app/api/admin/stats/route.ts
  27. 184 0
      src/app/api/admin/students/[id]/route.ts
  28. 126 0
      src/app/api/admin/students/route.ts
  29. 184 0
      src/app/api/admin/teachers/[id]/route.ts
  30. 125 0
      src/app/api/admin/teachers/route.ts
  31. 6 0
      src/app/api/auth/[...nextauth]/route.ts
  32. 99 0
      src/app/auth/signin/page.tsx
  33. 109 13
      src/app/globals.css
  34. 11 13
      src/app/layout.tsx
  35. 66 97
      src/app/page.tsx
  36. 12 0
      src/components/providers/auth-provider.tsx
  37. 66 0
      src/components/ui/alert.tsx
  38. 59 0
      src/components/ui/button.tsx
  39. 92 0
      src/components/ui/card.tsx
  40. 21 0
      src/components/ui/input.tsx
  41. 24 0
      src/components/ui/label.tsx
  42. 185 0
      src/components/ui/select.tsx
  43. 78 0
      src/lib/auth.ts
  44. 10 0
      src/lib/db/index.ts
  45. 199 0
      src/lib/db/schema.ts
  46. 157 0
      src/lib/db/seed.ts
  47. 6 0
      src/lib/utils.ts
  48. 48 0
      src/middleware.ts
  49. 25 0
      src/types/next-auth.d.ts

+ 21 - 0
components.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "new-york",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "",
+    "css": "src/app/globals.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils",
+    "ui": "@/components/ui",
+    "lib": "@/lib",
+    "hooks": "@/hooks"
+  },
+  "iconLibrary": "lucide"
+}

+ 10 - 0
drizzle.config.ts

@@ -0,0 +1,10 @@
+import { defineConfig } from 'drizzle-kit';
+
+export default defineConfig({
+  schema: './src/lib/db/schema.ts',
+  out: './drizzle',
+  dialect: 'postgresql',
+  dbCredentials: {
+    url: process.env.DATABASE_URL!,
+  },
+});

+ 99 - 0
drizzle/0000_goofy_obadiah_stane.sql

@@ -0,0 +1,99 @@
+CREATE TABLE "attendance" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"student_id" uuid,
+	"class_id" uuid,
+	"section_id" uuid,
+	"teacher_id" uuid,
+	"date" date NOT NULL,
+	"status" varchar(20) NOT NULL,
+	"reason" text,
+	"created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "classes" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(100) NOT NULL,
+	"code" varchar(20) NOT NULL,
+	"period_id" uuid,
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now(),
+	"updated_at" timestamp DEFAULT now(),
+	CONSTRAINT "classes_code_unique" UNIQUE("code")
+);
+--> statement-breakpoint
+CREATE TABLE "partials" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(100) NOT NULL,
+	"period_id" uuid,
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now(),
+	"updated_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "periods" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(100) NOT NULL,
+	"start_date" date NOT NULL,
+	"end_date" date NOT NULL,
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now(),
+	"updated_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "sections" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"name" varchar(50) NOT NULL,
+	"class_id" uuid,
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now(),
+	"updated_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "student_enrollments" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"student_id" uuid,
+	"class_id" uuid,
+	"section_id" uuid,
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "teacher_assignments" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"teacher_id" uuid,
+	"class_id" uuid,
+	"section_id" uuid,
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "users" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"email" varchar(255) NOT NULL,
+	"password" text NOT NULL,
+	"first_name" varchar(100) NOT NULL,
+	"last_name" varchar(100) NOT NULL,
+	"cedula" varchar(20) NOT NULL,
+	"phone" varchar(20),
+	"role" varchar(20) NOT NULL,
+	"admission_number" varchar(50),
+	"is_active" boolean DEFAULT true,
+	"created_at" timestamp DEFAULT now(),
+	"updated_at" timestamp DEFAULT now(),
+	CONSTRAINT "users_email_unique" UNIQUE("email"),
+	CONSTRAINT "users_cedula_unique" UNIQUE("cedula")
+);
+--> statement-breakpoint
+ALTER TABLE "attendance" ADD CONSTRAINT "attendance_student_id_users_id_fk" FOREIGN KEY ("student_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "attendance" ADD CONSTRAINT "attendance_class_id_classes_id_fk" FOREIGN KEY ("class_id") REFERENCES "public"."classes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "attendance" ADD CONSTRAINT "attendance_section_id_sections_id_fk" FOREIGN KEY ("section_id") REFERENCES "public"."sections"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "attendance" ADD CONSTRAINT "attendance_teacher_id_users_id_fk" FOREIGN KEY ("teacher_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "classes" ADD CONSTRAINT "classes_period_id_periods_id_fk" FOREIGN KEY ("period_id") REFERENCES "public"."periods"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "partials" ADD CONSTRAINT "partials_period_id_periods_id_fk" FOREIGN KEY ("period_id") REFERENCES "public"."periods"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "sections" ADD CONSTRAINT "sections_class_id_classes_id_fk" FOREIGN KEY ("class_id") REFERENCES "public"."classes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "student_enrollments" ADD CONSTRAINT "student_enrollments_student_id_users_id_fk" FOREIGN KEY ("student_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "student_enrollments" ADD CONSTRAINT "student_enrollments_class_id_classes_id_fk" FOREIGN KEY ("class_id") REFERENCES "public"."classes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "student_enrollments" ADD CONSTRAINT "student_enrollments_section_id_sections_id_fk" FOREIGN KEY ("section_id") REFERENCES "public"."sections"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "teacher_assignments" ADD CONSTRAINT "teacher_assignments_teacher_id_users_id_fk" FOREIGN KEY ("teacher_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "teacher_assignments" ADD CONSTRAINT "teacher_assignments_class_id_classes_id_fk" FOREIGN KEY ("class_id") REFERENCES "public"."classes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "teacher_assignments" ADD CONSTRAINT "teacher_assignments_section_id_sections_id_fk" FOREIGN KEY ("section_id") REFERENCES "public"."sections"("id") ON DELETE no action ON UPDATE no action;

+ 12 - 0
drizzle/0001_normal_juggernaut.sql

@@ -0,0 +1,12 @@
+ALTER TABLE "classes" DROP CONSTRAINT "classes_period_id_periods_id_fk";
+--> statement-breakpoint
+ALTER TABLE "attendance" ADD COLUMN "partial_id" uuid;--> statement-breakpoint
+ALTER TABLE "classes" ADD COLUMN "credits" integer NOT NULL;--> statement-breakpoint
+ALTER TABLE "classes" ADD COLUMN "description" text;--> statement-breakpoint
+ALTER TABLE "partials" ADD COLUMN "start_date" date NOT NULL;--> statement-breakpoint
+ALTER TABLE "partials" ADD COLUMN "end_date" date NOT NULL;--> statement-breakpoint
+ALTER TABLE "sections" ADD COLUMN "period_id" uuid;--> statement-breakpoint
+ALTER TABLE "sections" ADD COLUMN "max_students" integer NOT NULL;--> statement-breakpoint
+ALTER TABLE "attendance" ADD CONSTRAINT "attendance_partial_id_partials_id_fk" FOREIGN KEY ("partial_id") REFERENCES "public"."partials"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "sections" ADD CONSTRAINT "sections_period_id_periods_id_fk" FOREIGN KEY ("period_id") REFERENCES "public"."periods"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "classes" DROP COLUMN "period_id";

+ 703 - 0
drizzle/meta/0000_snapshot.json

@@ -0,0 +1,703 @@
+{
+  "id": "0dec0586-cf88-4f21-84bf-aaf73e14e71b",
+  "prevId": "00000000-0000-0000-0000-000000000000",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.attendance": {
+      "name": "attendance",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "student_id": {
+          "name": "student_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "section_id": {
+          "name": "section_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "teacher_id": {
+          "name": "teacher_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "date": {
+          "name": "date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "status": {
+          "name": "status",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "reason": {
+          "name": "reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "attendance_student_id_users_id_fk": {
+          "name": "attendance_student_id_users_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "users",
+          "columnsFrom": [
+            "student_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_class_id_classes_id_fk": {
+          "name": "attendance_class_id_classes_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_section_id_sections_id_fk": {
+          "name": "attendance_section_id_sections_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "sections",
+          "columnsFrom": [
+            "section_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_teacher_id_users_id_fk": {
+          "name": "attendance_teacher_id_users_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "users",
+          "columnsFrom": [
+            "teacher_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.classes": {
+      "name": "classes",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "code": {
+          "name": "code",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "period_id": {
+          "name": "period_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "classes_period_id_periods_id_fk": {
+          "name": "classes_period_id_periods_id_fk",
+          "tableFrom": "classes",
+          "tableTo": "periods",
+          "columnsFrom": [
+            "period_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "classes_code_unique": {
+          "name": "classes_code_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "code"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.partials": {
+      "name": "partials",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "period_id": {
+          "name": "period_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "partials_period_id_periods_id_fk": {
+          "name": "partials_period_id_periods_id_fk",
+          "tableFrom": "partials",
+          "tableTo": "periods",
+          "columnsFrom": [
+            "period_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.periods": {
+      "name": "periods",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "start_date": {
+          "name": "start_date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "end_date": {
+          "name": "end_date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sections": {
+      "name": "sections",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "sections_class_id_classes_id_fk": {
+          "name": "sections_class_id_classes_id_fk",
+          "tableFrom": "sections",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.student_enrollments": {
+      "name": "student_enrollments",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "student_id": {
+          "name": "student_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "section_id": {
+          "name": "section_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "student_enrollments_student_id_users_id_fk": {
+          "name": "student_enrollments_student_id_users_id_fk",
+          "tableFrom": "student_enrollments",
+          "tableTo": "users",
+          "columnsFrom": [
+            "student_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "student_enrollments_class_id_classes_id_fk": {
+          "name": "student_enrollments_class_id_classes_id_fk",
+          "tableFrom": "student_enrollments",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "student_enrollments_section_id_sections_id_fk": {
+          "name": "student_enrollments_section_id_sections_id_fk",
+          "tableFrom": "student_enrollments",
+          "tableTo": "sections",
+          "columnsFrom": [
+            "section_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.teacher_assignments": {
+      "name": "teacher_assignments",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "teacher_id": {
+          "name": "teacher_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "section_id": {
+          "name": "section_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "teacher_assignments_teacher_id_users_id_fk": {
+          "name": "teacher_assignments_teacher_id_users_id_fk",
+          "tableFrom": "teacher_assignments",
+          "tableTo": "users",
+          "columnsFrom": [
+            "teacher_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "teacher_assignments_class_id_classes_id_fk": {
+          "name": "teacher_assignments_class_id_classes_id_fk",
+          "tableFrom": "teacher_assignments",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "teacher_assignments_section_id_sections_id_fk": {
+          "name": "teacher_assignments_section_id_sections_id_fk",
+          "tableFrom": "teacher_assignments",
+          "tableTo": "sections",
+          "columnsFrom": [
+            "section_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "email": {
+          "name": "email",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "password": {
+          "name": "password",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "first_name": {
+          "name": "first_name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "last_name": {
+          "name": "last_name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "cedula": {
+          "name": "cedula",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "phone": {
+          "name": "phone",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "admission_number": {
+          "name": "admission_number",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "users_email_unique": {
+          "name": "users_email_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "email"
+          ]
+        },
+        "users_cedula_unique": {
+          "name": "users_cedula_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "cedula"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 751 - 0
drizzle/meta/0001_snapshot.json

@@ -0,0 +1,751 @@
+{
+  "id": "a52e7c22-a395-4c42-b9eb-2093435413bb",
+  "prevId": "0dec0586-cf88-4f21-84bf-aaf73e14e71b",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.attendance": {
+      "name": "attendance",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "student_id": {
+          "name": "student_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "section_id": {
+          "name": "section_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "teacher_id": {
+          "name": "teacher_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "partial_id": {
+          "name": "partial_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "date": {
+          "name": "date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "status": {
+          "name": "status",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "reason": {
+          "name": "reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "attendance_student_id_users_id_fk": {
+          "name": "attendance_student_id_users_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "users",
+          "columnsFrom": [
+            "student_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_class_id_classes_id_fk": {
+          "name": "attendance_class_id_classes_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_section_id_sections_id_fk": {
+          "name": "attendance_section_id_sections_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "sections",
+          "columnsFrom": [
+            "section_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_teacher_id_users_id_fk": {
+          "name": "attendance_teacher_id_users_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "users",
+          "columnsFrom": [
+            "teacher_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "attendance_partial_id_partials_id_fk": {
+          "name": "attendance_partial_id_partials_id_fk",
+          "tableFrom": "attendance",
+          "tableTo": "partials",
+          "columnsFrom": [
+            "partial_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.classes": {
+      "name": "classes",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "code": {
+          "name": "code",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "credits": {
+          "name": "credits",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "classes_code_unique": {
+          "name": "classes_code_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "code"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.partials": {
+      "name": "partials",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "period_id": {
+          "name": "period_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "start_date": {
+          "name": "start_date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "end_date": {
+          "name": "end_date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "partials_period_id_periods_id_fk": {
+          "name": "partials_period_id_periods_id_fk",
+          "tableFrom": "partials",
+          "tableTo": "periods",
+          "columnsFrom": [
+            "period_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.periods": {
+      "name": "periods",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "start_date": {
+          "name": "start_date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "end_date": {
+          "name": "end_date",
+          "type": "date",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sections": {
+      "name": "sections",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "period_id": {
+          "name": "period_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "max_students": {
+          "name": "max_students",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "sections_class_id_classes_id_fk": {
+          "name": "sections_class_id_classes_id_fk",
+          "tableFrom": "sections",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "sections_period_id_periods_id_fk": {
+          "name": "sections_period_id_periods_id_fk",
+          "tableFrom": "sections",
+          "tableTo": "periods",
+          "columnsFrom": [
+            "period_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.student_enrollments": {
+      "name": "student_enrollments",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "student_id": {
+          "name": "student_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "section_id": {
+          "name": "section_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "student_enrollments_student_id_users_id_fk": {
+          "name": "student_enrollments_student_id_users_id_fk",
+          "tableFrom": "student_enrollments",
+          "tableTo": "users",
+          "columnsFrom": [
+            "student_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "student_enrollments_class_id_classes_id_fk": {
+          "name": "student_enrollments_class_id_classes_id_fk",
+          "tableFrom": "student_enrollments",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "student_enrollments_section_id_sections_id_fk": {
+          "name": "student_enrollments_section_id_sections_id_fk",
+          "tableFrom": "student_enrollments",
+          "tableTo": "sections",
+          "columnsFrom": [
+            "section_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.teacher_assignments": {
+      "name": "teacher_assignments",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "teacher_id": {
+          "name": "teacher_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "class_id": {
+          "name": "class_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "section_id": {
+          "name": "section_id",
+          "type": "uuid",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "teacher_assignments_teacher_id_users_id_fk": {
+          "name": "teacher_assignments_teacher_id_users_id_fk",
+          "tableFrom": "teacher_assignments",
+          "tableTo": "users",
+          "columnsFrom": [
+            "teacher_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "teacher_assignments_class_id_classes_id_fk": {
+          "name": "teacher_assignments_class_id_classes_id_fk",
+          "tableFrom": "teacher_assignments",
+          "tableTo": "classes",
+          "columnsFrom": [
+            "class_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        },
+        "teacher_assignments_section_id_sections_id_fk": {
+          "name": "teacher_assignments_section_id_sections_id_fk",
+          "tableFrom": "teacher_assignments",
+          "tableTo": "sections",
+          "columnsFrom": [
+            "section_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "no action",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "uuid",
+          "primaryKey": true,
+          "notNull": true,
+          "default": "gen_random_uuid()"
+        },
+        "email": {
+          "name": "email",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "password": {
+          "name": "password",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "first_name": {
+          "name": "first_name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "last_name": {
+          "name": "last_name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "cedula": {
+          "name": "cedula",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "phone": {
+          "name": "phone",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "admission_number": {
+          "name": "admission_number",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_active": {
+          "name": "is_active",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {
+        "users_email_unique": {
+          "name": "users_email_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "email"
+          ]
+        },
+        "users_cedula_unique": {
+          "name": "users_cedula_unique",
+          "nullsNotDistinct": false,
+          "columns": [
+            "cedula"
+          ]
+        }
+      },
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 20 - 0
drizzle/meta/_journal.json

@@ -0,0 +1,20 @@
+{
+  "version": "7",
+  "dialect": "postgresql",
+  "entries": [
+    {
+      "idx": 0,
+      "version": "7",
+      "when": 1755502816226,
+      "tag": "0000_goofy_obadiah_stane",
+      "breakpoints": true
+    },
+    {
+      "idx": 1,
+      "version": "7",
+      "when": 1755504262104,
+      "tag": "0001_normal_juggernaut",
+      "breakpoints": true
+    }
+  ]
+}

File diff suppressed because it is too large
+ 2157 - 586
package-lock.json


+ 26 - 7
package.json

@@ -3,25 +3,44 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "dev": "next dev --turbopack",
+    "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "db:generate": "drizzle-kit generate",
+    "db:migrate": "drizzle-kit migrate",
+    "db:studio": "drizzle-kit studio",
+    "db:seed": "tsx src/lib/db/seed.ts"
   },
   "dependencies": {
+    "@auth/drizzle-adapter": "^1.10.0",
+    "@radix-ui/react-label": "^2.1.7",
+    "@radix-ui/react-slot": "^1.2.3",
+    "@types/bcryptjs": "^2.4.6",
+    "@types/pg": "^8.15.5",
+    "bcryptjs": "^3.0.2",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "drizzle-kit": "^0.31.4",
+    "drizzle-orm": "^0.44.4",
+    "lucide-react": "^0.539.0",
+    "next": "15.4.6",
+    "next-auth": "^4.24.11",
+    "postgres": "^3.4.7",
     "react": "19.1.0",
     "react-dom": "19.1.0",
-    "next": "15.4.6"
+    "tailwind-merge": "^3.3.1"
   },
   "devDependencies": {
-    "typescript": "^5",
+    "@eslint/eslintrc": "^3",
+    "@tailwindcss/postcss": "^4",
     "@types/node": "^20",
     "@types/react": "^19",
     "@types/react-dom": "^19",
-    "@tailwindcss/postcss": "^4",
-    "tailwindcss": "^4",
     "eslint": "^9",
     "eslint-config-next": "15.4.6",
-    "@eslint/eslintrc": "^3"
+    "tailwindcss": "^4",
+    "tw-animate-css": "^1.3.7",
+    "typescript": "^5"
   }
 }

+ 295 - 0
src/app/admin/classes/page.tsx

@@ -0,0 +1,295 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Trash2, Edit, Plus, BookOpen } from 'lucide-react';
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  credits: number;
+  description?: string;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface FormData {
+  name: string;
+  code: string;
+  credits: string;
+  description: string;
+}
+
+export default function ClassesPage() {
+  const [classes, setClasses] = useState<Class[]>([]);
+  const [formData, setFormData] = useState<FormData>({
+    name: '',
+    code: '',
+    credits: '',
+    description: ''
+  });
+  const [editingId, setEditingId] = useState<string | null>(null);
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    fetchClasses();
+  }, []);
+
+  const fetchClasses = async () => {
+    try {
+      const response = await fetch('/api/admin/classes');
+      if (response.ok) {
+        const data = await response.json();
+        setClasses(data);
+      }
+    } catch (error) {
+      console.error('Error fetching classes:', error);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setError('');
+    setSuccess('');
+
+    try {
+      const credits = parseInt(formData.credits);
+      if (isNaN(credits) || credits < 1) {
+        setError('Los créditos deben ser un número mayor a 0');
+        setLoading(false);
+        return;
+      }
+
+      const url = editingId ? `/api/admin/classes/${editingId}` : '/api/admin/classes';
+      const method = editingId ? 'PUT' : 'POST';
+
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          name: formData.name,
+          code: formData.code,
+          credits,
+          description: formData.description || undefined
+        }),
+      });
+
+      if (response.ok) {
+        setSuccess(editingId ? 'Clase actualizada exitosamente' : 'Clase creada exitosamente');
+        setFormData({ name: '', code: '', credits: '', description: '' });
+        setEditingId(null);
+        fetchClasses();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al procesar la solicitud');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleEdit = (classItem: Class) => {
+    setFormData({
+      name: classItem.name,
+      code: classItem.code,
+      credits: classItem.credits.toString(),
+      description: classItem.description || ''
+    });
+    setEditingId(classItem.id);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleDelete = async (id: string) => {
+    if (!confirm('¿Estás seguro de que deseas eliminar esta clase?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/classes/${id}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        setSuccess('Clase eliminada exitosamente');
+        fetchClasses();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al eliminar la clase');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleCancel = () => {
+    setFormData({ name: '', code: '', credits: '', description: '' });
+    setEditingId(null);
+    setError('');
+    setSuccess('');
+  };
+
+  return (
+    <div className="container mx-auto p-6">
+      <div className="flex items-center gap-2 mb-6">
+        <BookOpen className="h-6 w-6" />
+        <h1 className="text-2xl font-bold">Gestión de Clases</h1>
+      </div>
+
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+        {/* Formulario */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Plus className="h-5 w-5" />
+              {editingId ? 'Editar Clase' : 'Nueva Clase'}
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleSubmit} className="space-y-4">
+              <div>
+                <Label htmlFor="name">Nombre de la Clase</Label>
+                <Input
+                  id="name"
+                  type="text"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  required
+                  placeholder="Ej: Matemáticas I"
+                />
+              </div>
+
+              <div>
+                <Label htmlFor="code">Código</Label>
+                <Input
+                  id="code"
+                  type="text"
+                  value={formData.code}
+                  onChange={(e) => setFormData({ ...formData, code: e.target.value })}
+                  required
+                  placeholder="Ej: MAT101"
+                />
+              </div>
+
+              <div>
+                <Label htmlFor="credits">Créditos</Label>
+                <Input
+                  id="credits"
+                  type="number"
+                  min="1"
+                  value={formData.credits}
+                  onChange={(e) => setFormData({ ...formData, credits: e.target.value })}
+                  required
+                  placeholder="Ej: 3"
+                />
+              </div>
+
+              <div>
+                <Label htmlFor="description">Descripción (Opcional)</Label>
+                <Input
+                  id="description"
+                  type="text"
+                  value={formData.description}
+                  onChange={(e) => setFormData({ ...formData, description: e.target.value })}
+                  placeholder="Descripción de la clase"
+                />
+              </div>
+
+              <div className="flex gap-2">
+                <Button type="submit" disabled={loading}>
+                  {loading ? 'Procesando...' : (editingId ? 'Actualizar' : 'Crear')}
+                </Button>
+                {editingId && (
+                  <Button type="button" variant="outline" onClick={handleCancel}>
+                    Cancelar
+                  </Button>
+                )}
+              </div>
+            </form>
+
+            {error && (
+              <Alert className="mt-4 border-red-200 bg-red-50">
+                <AlertDescription className="text-red-800">{error}</AlertDescription>
+              </Alert>
+            )}
+
+            {success && (
+              <Alert className="mt-4 border-green-200 bg-green-50">
+                <AlertDescription className="text-green-800">{success}</AlertDescription>
+              </Alert>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Lista de Clases */}
+        <Card>
+          <CardHeader>
+            <CardTitle>Clases Registradas ({classes.length})</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-3 max-h-96 overflow-y-auto">
+              {classes.length === 0 ? (
+                <p className="text-gray-500 text-center py-4">No hay clases registradas</p>
+              ) : (
+                classes.map((classItem) => (
+                  <div
+                    key={classItem.id}
+                    className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
+                  >
+                    <div className="flex-1">
+                      <div className="flex items-center gap-2">
+                        <h3 className="font-medium">{classItem.name}</h3>
+                        <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
+                          {classItem.code}
+                        </span>
+                        <span className="text-sm bg-gray-100 text-gray-800 px-2 py-1 rounded">
+                          {classItem.credits} créditos
+                        </span>
+                      </div>
+                      {classItem.description && (
+                        <p className="text-sm text-gray-600 mt-1">{classItem.description}</p>
+                      )}
+                      <p className="text-xs text-gray-500 mt-1">
+                        Estado: {classItem.isActive ? 'Activa' : 'Inactiva'}
+                      </p>
+                    </div>
+                    <div className="flex gap-2">
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleEdit(classItem)}
+                      >
+                        <Edit className="h-4 w-4" />
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleDelete(classItem.id)}
+                        className="text-red-600 hover:text-red-700"
+                      >
+                        <Trash2 className="h-4 w-4" />
+                      </Button>
+                    </div>
+                  </div>
+                ))
+              )}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 277 - 0
src/app/admin/dashboard/page.tsx

@@ -0,0 +1,277 @@
+'use client';
+
+import { useSession } from 'next-auth/react';
+import { useEffect, useState } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Users, GraduationCap, BookOpen, Calendar, FileText, School, Building, Clock } from 'lucide-react';
+import Link from 'next/link';
+
+interface DashboardStats {
+  teachers: number;
+  students: number;
+  classes: number;
+  sections: number;
+  periods: number;
+  partials: number;
+}
+
+export default function AdminDashboard() {
+  const { data: session } = useSession();
+  const [stats, setStats] = useState<DashboardStats>({
+    teachers: 0,
+    students: 0,
+    classes: 0,
+    sections: 0,
+    periods: 0,
+    partials: 0,
+  });
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    fetchStats();
+  }, []);
+
+  const fetchStats = async () => {
+    try {
+      const response = await fetch('/api/admin/stats');
+      if (response.ok) {
+        const data = await response.json();
+        setStats(data);
+      }
+    } catch (error) {
+      console.error('Error fetching stats:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const statCards = [
+    {
+      title: 'Profesores',
+      value: stats.teachers,
+      icon: Users,
+      color: 'text-blue-600',
+      bgColor: 'bg-blue-50',
+      href: '/admin/teachers',
+    },
+    {
+      title: 'Estudiantes',
+      value: stats.students,
+      icon: GraduationCap,
+      color: 'text-green-600',
+      bgColor: 'bg-green-50',
+      href: '/admin/students',
+    },
+    {
+      title: 'Clases',
+      value: stats.classes,
+      icon: BookOpen,
+      color: 'text-purple-600',
+      bgColor: 'bg-purple-50',
+      href: '/admin/classes',
+    },
+    {
+      title: 'Paralelos',
+      value: stats.sections,
+      icon: School,
+      color: 'text-orange-600',
+      bgColor: 'bg-orange-50',
+      href: '/admin/sections',
+    },
+    {
+      title: 'Periodos',
+      value: stats.periods,
+      icon: Calendar,
+      color: 'text-red-600',
+      bgColor: 'bg-red-50',
+      href: '/admin/periods',
+    },
+    {
+      title: 'Parciales',
+      value: stats.partials,
+      icon: FileText,
+      color: 'text-indigo-600',
+      bgColor: 'bg-indigo-50',
+      href: '/admin/partials',
+    },
+  ];
+
+  if (loading) {
+    return (
+      <div className="min-h-screen flex items-center justify-center">
+        <div className="text-lg">Cargando dashboard...</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white shadow">
+        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+          <div className="flex justify-between items-center py-6">
+            <div>
+              <h1 className="text-3xl font-bold text-gray-900">
+                Dashboard Administrativo
+              </h1>
+              <p className="text-gray-600">
+                Bienvenido, {session?.user?.name}
+              </p>
+            </div>
+            <Button 
+              onClick={() => window.location.href = '/api/auth/signout'}
+              variant="outline"
+            >
+              Cerrar Sesión
+            </Button>
+          </div>
+        </div>
+      </div>
+
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
+          {statCards.map((card) => {
+            const Icon = card.icon;
+            return (
+              <Link key={card.title} href={card.href}>
+                <Card className="hover:shadow-lg transition-shadow cursor-pointer">
+                  <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+                    <CardTitle className="text-sm font-medium">
+                      {card.title}
+                    </CardTitle>
+                    <div className={`p-2 rounded-full ${card.bgColor}`}>
+                      <Icon className={`h-4 w-4 ${card.color}`} />
+                    </div>
+                  </CardHeader>
+                  <CardContent>
+                    <div className="text-2xl font-bold">{card.value}</div>
+                    <p className="text-xs text-muted-foreground">
+                      Total registrado
+                    </p>
+                  </CardContent>
+                </Card>
+              </Link>
+            );
+          })}
+        </div>
+
+        <div className="mb-8">
+          <h2 className="text-2xl font-bold text-gray-900 mb-6">Gestión del Sistema</h2>
+          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+            <Link href="/admin/teachers">
+              <Card className="hover:shadow-md transition-shadow cursor-pointer">
+                <CardContent className="p-6">
+                  <div className="flex items-center gap-4">
+                    <Users className="h-8 w-8 text-blue-600" />
+                    <div>
+                      <h3 className="font-semibold">Gestionar Profesores</h3>
+                      <p className="text-sm text-gray-600">Crear, editar y eliminar profesores</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            </Link>
+            
+            <Link href="/admin/students">
+              <Card className="hover:shadow-md transition-shadow cursor-pointer">
+                <CardContent className="p-6">
+                  <div className="flex items-center gap-4">
+                    <GraduationCap className="h-8 w-8 text-green-600" />
+                    <div>
+                      <h3 className="font-semibold">Gestionar Estudiantes</h3>
+                      <p className="text-sm text-gray-600">Crear, editar y eliminar estudiantes</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            </Link>
+            
+            <Link href="/admin/periods">
+              <Card className="hover:shadow-md transition-shadow cursor-pointer">
+                <CardContent className="p-6">
+                  <div className="flex items-center gap-4">
+                    <Calendar className="h-8 w-8 text-purple-600" />
+                    <div>
+                      <h3 className="font-semibold">Períodos Académicos</h3>
+                      <p className="text-sm text-gray-600">Gestionar períodos académicos</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            </Link>
+            
+            <Link href="/admin/classes">
+              <Card className="hover:shadow-md transition-shadow cursor-pointer">
+                <CardContent className="p-6">
+                  <div className="flex items-center gap-4">
+                    <BookOpen className="h-8 w-8 text-orange-600" />
+                    <div>
+                      <h3 className="font-semibold">Gestionar Clases</h3>
+                      <p className="text-sm text-gray-600">Crear y administrar clases</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            </Link>
+            
+            <Link href="/admin/sections">
+              <Card className="hover:shadow-md transition-shadow cursor-pointer">
+                <CardContent className="p-6">
+                  <div className="flex items-center gap-4">
+                    <Building className="h-8 w-8 text-indigo-600" />
+                    <div>
+                      <h3 className="font-semibold">Gestionar Paralelos</h3>
+                      <p className="text-sm text-gray-600">Crear y administrar paralelos</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            </Link>
+            
+            <Link href="/admin/partials">
+              <Card className="hover:shadow-md transition-shadow cursor-pointer">
+                <CardContent className="p-6">
+                  <div className="flex items-center gap-4">
+                    <Clock className="h-8 w-8 text-red-600" />
+                    <div>
+                      <h3 className="font-semibold">Gestionar Parciales</h3>
+                      <p className="text-sm text-gray-600">Configurar períodos de parciales</p>
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            </Link>
+          </div>
+        </div>
+
+        <div className="grid grid-cols-1 lg:grid-cols-1 gap-6">
+          <Card>
+            <CardHeader>
+              <CardTitle>Información del Sistema</CardTitle>
+              <CardDescription>
+                Estado actual del sistema de gestión
+              </CardDescription>
+            </CardHeader>
+            <CardContent>
+              <div className="space-y-4">
+                <div className="flex justify-between items-center">
+                  <span className="text-sm font-medium">Estado del Sistema</span>
+                  <span className="text-sm text-green-600 font-medium">Activo</span>
+                </div>
+                <div className="flex justify-between items-center">
+                  <span className="text-sm font-medium">Última Actualización</span>
+                  <span className="text-sm text-gray-600">{new Date().toLocaleDateString()}</span>
+                </div>
+                <div className="flex justify-between items-center">
+                  <span className="text-sm font-medium">Versión</span>
+                  <span className="text-sm text-gray-600">1.0.0</span>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+
+        </div>
+      </div>
+    </div>
+  );
+}

+ 325 - 0
src/app/admin/partials/page.tsx

@@ -0,0 +1,325 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Trash2, Edit, Plus, Calendar } from 'lucide-react';
+
+interface Partial {
+  id: string;
+  name: string;
+  periodId: string;
+  periodName: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface Period {
+  id: string;
+  name: string;
+  isActive?: boolean;
+}
+
+interface FormData {
+  name: string;
+  periodId: string;
+  startDate: string;
+  endDate: string;
+}
+
+export default function PartialsPage() {
+  const [partials, setPartials] = useState<Partial[]>([]);
+  const [periods, setPeriods] = useState<Period[]>([]);
+  const [formData, setFormData] = useState<FormData>({
+    name: '',
+    periodId: '',
+    startDate: '',
+    endDate: ''
+  });
+  const [editingId, setEditingId] = useState<string | null>(null);
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    fetchPartials();
+    fetchPeriods();
+  }, []);
+
+  const fetchPartials = async () => {
+    try {
+      const response = await fetch('/api/admin/partials');
+      if (response.ok) {
+        const data = await response.json();
+        setPartials(data);
+      }
+    } catch (error) {
+      console.error('Error fetching partials:', error);
+    }
+  };
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/periods');
+      if (response.ok) {
+        const data = await response.json();
+        setPeriods(data.filter((p: Period) => p.isActive !== false));
+      }
+    } catch (error) {
+      console.error('Error fetching periods:', error);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setError('');
+    setSuccess('');
+
+    try {
+      const startDate = new Date(formData.startDate);
+      const endDate = new Date(formData.endDate);
+
+      if (startDate >= endDate) {
+        setError('La fecha de inicio debe ser anterior a la fecha de fin');
+        setLoading(false);
+        return;
+      }
+
+      const url = editingId ? `/api/admin/partials/${editingId}` : '/api/admin/partials';
+      const method = editingId ? 'PUT' : 'POST';
+
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          name: formData.name,
+          periodId: formData.periodId,
+          startDate: formData.startDate,
+          endDate: formData.endDate
+        }),
+      });
+
+      if (response.ok) {
+        setSuccess(editingId ? 'Parcial actualizado exitosamente' : 'Parcial creado exitosamente');
+        setFormData({ name: '', periodId: '', startDate: '', endDate: '' });
+        setEditingId(null);
+        fetchPartials();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al procesar la solicitud');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleEdit = (partial: Partial) => {
+    setFormData({
+      name: partial.name,
+      periodId: partial.periodId,
+      startDate: partial.startDate,
+      endDate: partial.endDate
+    });
+    setEditingId(partial.id);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleDelete = async (id: string) => {
+    if (!confirm('¿Estás seguro de que deseas eliminar este parcial?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/partials/${id}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        setSuccess('Parcial eliminado exitosamente');
+        fetchPartials();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al eliminar el parcial');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleCancel = () => {
+    setFormData({ name: '', periodId: '', startDate: '', endDate: '' });
+    setEditingId(null);
+    setError('');
+    setSuccess('');
+  };
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES');
+  };
+
+  return (
+    <div className="container mx-auto p-6">
+      <div className="flex items-center gap-2 mb-6">
+        <Calendar className="h-6 w-6" />
+        <h1 className="text-2xl font-bold">Gestión de Parciales</h1>
+      </div>
+
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+        {/* Formulario */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Plus className="h-5 w-5" />
+              {editingId ? 'Editar Parcial' : 'Nuevo Parcial'}
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleSubmit} className="space-y-4">
+              <div>
+                <Label htmlFor="name">Nombre del Parcial</Label>
+                <Input
+                  id="name"
+                  type="text"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  required
+                  placeholder="Ej: Primer Parcial, Segundo Parcial"
+                />
+              </div>
+
+              <div>
+                <Label htmlFor="periodId">Período Académico</Label>
+                <Select
+                  value={formData.periodId}
+                  onValueChange={(value) => setFormData({ ...formData, periodId: value })}
+                  required
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Seleccionar período" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div>
+                <Label htmlFor="startDate">Fecha de Inicio</Label>
+                <Input
+                  id="startDate"
+                  type="date"
+                  value={formData.startDate}
+                  onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
+                  required
+                />
+              </div>
+
+              <div>
+                <Label htmlFor="endDate">Fecha de Fin</Label>
+                <Input
+                  id="endDate"
+                  type="date"
+                  value={formData.endDate}
+                  onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
+                  required
+                />
+              </div>
+
+              <div className="flex gap-2">
+                <Button type="submit" disabled={loading}>
+                  {loading ? 'Procesando...' : (editingId ? 'Actualizar' : 'Crear')}
+                </Button>
+                {editingId && (
+                  <Button type="button" variant="outline" onClick={handleCancel}>
+                    Cancelar
+                  </Button>
+                )}
+              </div>
+            </form>
+
+            {error && (
+              <Alert className="mt-4 border-red-200 bg-red-50">
+                <AlertDescription className="text-red-800">{error}</AlertDescription>
+              </Alert>
+            )}
+
+            {success && (
+              <Alert className="mt-4 border-green-200 bg-green-50">
+                <AlertDescription className="text-green-800">{success}</AlertDescription>
+              </Alert>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Lista de Parciales */}
+        <Card>
+          <CardHeader>
+            <CardTitle>Parciales Registrados ({partials.length})</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-3 max-h-96 overflow-y-auto">
+              {partials.length === 0 ? (
+                <p className="text-gray-500 text-center py-4">No hay parciales registrados</p>
+              ) : (
+                partials.map((partial) => (
+                  <div
+                    key={partial.id}
+                    className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
+                  >
+                    <div className="flex-1">
+                      <div className="flex items-center gap-2">
+                        <h3 className="font-medium">{partial.name}</h3>
+                        <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
+                          {partial.periodName}
+                        </span>
+                      </div>
+                      <p className="text-sm text-gray-600 mt-1">
+                        {formatDate(partial.startDate)} - {formatDate(partial.endDate)}
+                      </p>
+                      <p className="text-xs text-gray-500 mt-1">
+                        Estado: {partial.isActive ? 'Activo' : 'Inactivo'}
+                      </p>
+                    </div>
+                    <div className="flex gap-2">
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleEdit(partial)}
+                      >
+                        <Edit className="h-4 w-4" />
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleDelete(partial.id)}
+                        className="text-red-600 hover:text-red-700"
+                      >
+                        <Trash2 className="h-4 w-4" />
+                      </Button>
+                    </div>
+                  </div>
+                ))
+              )}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 340 - 0
src/app/admin/periods/page.tsx

@@ -0,0 +1,340 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Plus, Edit, Trash2, Calendar, ArrowLeft } from 'lucide-react';
+import Link from 'next/link';
+
+interface Period {
+  id: string;
+  name: string;
+  startDate: string;
+  endDate: string;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface FormData {
+  name: string;
+  startDate: string;
+  endDate: string;
+}
+
+export default function PeriodsPage() {
+  const [periods, setPeriods] = useState<Period[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const [showForm, setShowForm] = useState(false);
+  const [editingPeriod, setEditingPeriod] = useState<Period | null>(null);
+  const [formData, setFormData] = useState<FormData>({
+    name: '',
+    startDate: '',
+    endDate: '',
+  });
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+
+  useEffect(() => {
+    fetchPeriods();
+  }, []);
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/periods');
+      if (response.ok) {
+        const data = await response.json();
+        setPeriods(data);
+      }
+    } catch (error) {
+      console.error('Error fetching periods:', error);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError('');
+    setSuccess('');
+
+    // Validar fechas
+    if (new Date(formData.startDate) >= new Date(formData.endDate)) {
+      setError('La fecha de inicio debe ser anterior a la fecha de fin');
+      return;
+    }
+
+    try {
+      const url = editingPeriod 
+        ? `/api/admin/periods/${editingPeriod.id}`
+        : '/api/admin/periods';
+      const method = editingPeriod ? 'PUT' : 'POST';
+
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        setSuccess(editingPeriod ? 'Período actualizado exitosamente' : 'Período creado exitosamente');
+        setShowForm(false);
+        setEditingPeriod(null);
+        setFormData({ name: '', startDate: '', endDate: '' });
+        fetchPeriods();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al procesar la solicitud');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleEdit = (period: Period) => {
+    setEditingPeriod(period);
+    setFormData({
+      name: period.name,
+      startDate: period.startDate.split('T')[0], // Formato YYYY-MM-DD
+      endDate: period.endDate.split('T')[0],
+    });
+    setShowForm(true);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleDelete = async (periodId: string) => {
+    if (!confirm('¿Estás seguro de que deseas eliminar este período académico?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/periods/${periodId}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        setSuccess('Período eliminado exitosamente');
+        fetchPeriods();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al eliminar el período');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleToggleActive = async (periodId: string, isActive: boolean) => {
+    try {
+      const response = await fetch(`/api/admin/periods/${periodId}/toggle`, {
+        method: 'PATCH',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ isActive: !isActive }),
+      });
+
+      if (response.ok) {
+        setSuccess(`Período ${!isActive ? 'activado' : 'desactivado'} exitosamente`);
+        fetchPeriods();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al cambiar el estado del período');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const resetForm = () => {
+    setShowForm(false);
+    setEditingPeriod(null);
+    setFormData({ name: '', startDate: '', endDate: '' });
+    setError('');
+    setSuccess('');
+  };
+
+  const formatDate = (dateString: string) => {
+    return new Date(dateString).toLocaleDateString('es-ES', {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric'
+    });
+  };
+
+  if (isLoading) {
+    return (
+      <div className="container mx-auto p-6">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg">Cargando períodos académicos...</div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="container mx-auto p-6">
+      <div className="flex items-center justify-between mb-6">
+        <div className="flex items-center gap-4">
+          <Link href="/admin/dashboard">
+            <Button variant="outline" size="sm">
+              <ArrowLeft className="h-4 w-4 mr-2" />
+              Volver al Dashboard
+            </Button>
+          </Link>
+          <div className="flex items-center gap-2">
+            <Calendar className="h-6 w-6" />
+            <h1 className="text-2xl font-bold">Gestión de Períodos Académicos</h1>
+          </div>
+        </div>
+        <Button onClick={() => setShowForm(true)} className="flex items-center gap-2">
+          <Plus className="h-4 w-4" />
+          Nuevo Período
+        </Button>
+      </div>
+
+      {error && (
+        <Alert className="mb-4 border-red-200 bg-red-50">
+          <AlertDescription className="text-red-800">{error}</AlertDescription>
+        </Alert>
+      )}
+
+      {success && (
+        <Alert className="mb-4 border-green-200 bg-green-50">
+          <AlertDescription className="text-green-800">{success}</AlertDescription>
+        </Alert>
+      )}
+
+      {showForm && (
+        <Card className="mb-6">
+          <CardHeader>
+            <CardTitle>
+              {editingPeriod ? 'Editar Período Académico' : 'Nuevo Período Académico'}
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleSubmit} className="space-y-4">
+              <div>
+                <Label htmlFor="name">Nombre del Período</Label>
+                <Input
+                  id="name"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  placeholder="Ej: 2024-1, Primer Semestre 2024"
+                  required
+                />
+              </div>
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                <div>
+                  <Label htmlFor="startDate">Fecha de Inicio</Label>
+                  <Input
+                    id="startDate"
+                    type="date"
+                    value={formData.startDate}
+                    onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
+                    required
+                  />
+                </div>
+                <div>
+                  <Label htmlFor="endDate">Fecha de Fin</Label>
+                  <Input
+                    id="endDate"
+                    type="date"
+                    value={formData.endDate}
+                    onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
+                    required
+                  />
+                </div>
+              </div>
+              <div className="flex gap-2">
+                <Button type="submit">
+                  {editingPeriod ? 'Actualizar' : 'Crear'} Período
+                </Button>
+                <Button type="button" variant="outline" onClick={resetForm}>
+                  Cancelar
+                </Button>
+              </div>
+            </form>
+          </CardContent>
+        </Card>
+      )}
+
+      <Card>
+        <CardHeader>
+          <CardTitle>Lista de Períodos Académicos ({periods.length})</CardTitle>
+        </CardHeader>
+        <CardContent>
+          {periods.length === 0 ? (
+            <div className="text-center py-8 text-gray-500">
+              No hay períodos académicos registrados
+            </div>
+          ) : (
+            <div className="overflow-x-auto">
+              <table className="w-full border-collapse">
+                <thead>
+                  <tr className="border-b">
+                    <th className="text-left p-2">Nombre</th>
+                    <th className="text-left p-2">Fecha de Inicio</th>
+                    <th className="text-left p-2">Fecha de Fin</th>
+                    <th className="text-left p-2">Estado</th>
+                    <th className="text-left p-2">Acciones</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {periods.map((period) => (
+                    <tr key={period.id} className="border-b hover:bg-gray-50">
+                      <td className="p-2 font-medium">{period.name}</td>
+                      <td className="p-2">{formatDate(period.startDate)}</td>
+                      <td className="p-2">{formatDate(period.endDate)}</td>
+                      <td className="p-2">
+                        <span className={`px-2 py-1 rounded-full text-xs ${
+                          period.isActive 
+                            ? 'bg-green-100 text-green-800' 
+                            : 'bg-red-100 text-red-800'
+                        }`}>
+                          {period.isActive ? 'Activo' : 'Inactivo'}
+                        </span>
+                      </td>
+                      <td className="p-2">
+                        <div className="flex gap-2">
+                          <Button
+                            size="sm"
+                            variant="outline"
+                            onClick={() => handleEdit(period)}
+                          >
+                            <Edit className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            size="sm"
+                            variant={period.isActive ? "outline" : "default"}
+                            onClick={() => handleToggleActive(period.id, period.isActive)}
+                            className={period.isActive ? "text-orange-600 hover:text-orange-700" : ""}
+                          >
+                            {period.isActive ? 'Desactivar' : 'Activar'}
+                          </Button>
+                          <Button
+                            size="sm"
+                            variant="outline"
+                            onClick={() => handleDelete(period.id)}
+                            className="text-red-600 hover:text-red-700"
+                          >
+                            <Trash2 className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

+ 353 - 0
src/app/admin/sections/page.tsx

@@ -0,0 +1,353 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Trash2, Edit, Plus, Users } from 'lucide-react';
+
+interface Section {
+  id: string;
+  name: string;
+  classId: string;
+  className: string;
+  classCode: string;
+  periodId: string;
+  periodName: string;
+  maxStudents: number;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface Class {
+  id: string;
+  name: string;
+  code: string;
+  isActive?: boolean;
+}
+
+interface Period {
+  id: string;
+  name: string;
+  isActive?: boolean;
+}
+
+interface FormData {
+  name: string;
+  classId: string;
+  periodId: string;
+  maxStudents: string;
+}
+
+export default function SectionsPage() {
+  const [sections, setSections] = useState<Section[]>([]);
+  const [classes, setClasses] = useState<Class[]>([]);
+  const [periods, setPeriods] = useState<Period[]>([]);
+  const [formData, setFormData] = useState<FormData>({
+    name: '',
+    classId: '',
+    periodId: '',
+    maxStudents: ''
+  });
+  const [editingId, setEditingId] = useState<string | null>(null);
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    fetchSections();
+    fetchClasses();
+    fetchPeriods();
+  }, []);
+
+  const fetchSections = async () => {
+    try {
+      const response = await fetch('/api/admin/sections');
+      if (response.ok) {
+        const data = await response.json();
+        setSections(data);
+      }
+    } catch (error) {
+      console.error('Error fetching sections:', error);
+    }
+  };
+
+  const fetchClasses = async () => {
+    try {
+      const response = await fetch('/api/admin/classes');
+      if (response.ok) {
+        const data = await response.json();
+        setClasses(data.filter((c: Class) => c.isActive !== false));
+      }
+    } catch (error) {
+      console.error('Error fetching classes:', error);
+    }
+  };
+
+  const fetchPeriods = async () => {
+    try {
+      const response = await fetch('/api/admin/periods');
+      if (response.ok) {
+        const data = await response.json();
+        setPeriods(data.filter((p: Period) => p.isActive !== false));
+      }
+    } catch (error) {
+      console.error('Error fetching periods:', error);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setError('');
+    setSuccess('');
+
+    try {
+      const maxStudents = parseInt(formData.maxStudents);
+      if (isNaN(maxStudents) || maxStudents < 1) {
+        setError('El número máximo de estudiantes debe ser mayor a 0');
+        setLoading(false);
+        return;
+      }
+
+      const url = editingId ? `/api/admin/sections/${editingId}` : '/api/admin/sections';
+      const method = editingId ? 'PUT' : 'POST';
+
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          name: formData.name,
+          classId: formData.classId,
+          periodId: formData.periodId,
+          maxStudents
+        }),
+      });
+
+      if (response.ok) {
+        setSuccess(editingId ? 'Sección actualizada exitosamente' : 'Sección creada exitosamente');
+        setFormData({ name: '', classId: '', periodId: '', maxStudents: '' });
+        setEditingId(null);
+        fetchSections();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al procesar la solicitud');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleEdit = (section: Section) => {
+    setFormData({
+      name: section.name,
+      classId: section.classId,
+      periodId: section.periodId,
+      maxStudents: section.maxStudents.toString()
+    });
+    setEditingId(section.id);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleDelete = async (id: string) => {
+    if (!confirm('¿Estás seguro de que deseas eliminar esta sección?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/sections/${id}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        setSuccess('Sección eliminada exitosamente');
+        fetchSections();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al eliminar la sección');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleCancel = () => {
+    setFormData({ name: '', classId: '', periodId: '', maxStudents: '' });
+    setEditingId(null);
+    setError('');
+    setSuccess('');
+  };
+
+  return (
+    <div className="container mx-auto p-6">
+      <div className="flex items-center gap-2 mb-6">
+        <Users className="h-6 w-6" />
+        <h1 className="text-2xl font-bold">Gestión de Secciones</h1>
+      </div>
+
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
+        {/* Formulario */}
+        <Card>
+          <CardHeader>
+            <CardTitle className="flex items-center gap-2">
+              <Plus className="h-5 w-5" />
+              {editingId ? 'Editar Sección' : 'Nueva Sección'}
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleSubmit} className="space-y-4">
+              <div>
+                <Label htmlFor="name">Nombre de la Sección</Label>
+                <Input
+                  id="name"
+                  type="text"
+                  value={formData.name}
+                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+                  required
+                  placeholder="Ej: A, B, 01, 02"
+                />
+              </div>
+
+              <div>
+                <Label htmlFor="classId">Clase</Label>
+                <Select
+                  value={formData.classId}
+                  onValueChange={(value) => setFormData({ ...formData, classId: value })}
+                  required
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Seleccionar clase" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {classes.map((classItem) => (
+                      <SelectItem key={classItem.id} value={classItem.id}>
+                        {classItem.code} - {classItem.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div>
+                <Label htmlFor="periodId">Período Académico</Label>
+                <Select
+                  value={formData.periodId}
+                  onValueChange={(value) => setFormData({ ...formData, periodId: value })}
+                  required
+                >
+                  <SelectTrigger>
+                    <SelectValue placeholder="Seleccionar período" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {periods.map((period) => (
+                      <SelectItem key={period.id} value={period.id}>
+                        {period.name}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div>
+                <Label htmlFor="maxStudents">Máximo de Estudiantes</Label>
+                <Input
+                  id="maxStudents"
+                  type="number"
+                  min="1"
+                  value={formData.maxStudents}
+                  onChange={(e) => setFormData({ ...formData, maxStudents: e.target.value })}
+                  required
+                  placeholder="Ej: 30"
+                />
+              </div>
+
+              <div className="flex gap-2">
+                <Button type="submit" disabled={loading}>
+                  {loading ? 'Procesando...' : (editingId ? 'Actualizar' : 'Crear')}
+                </Button>
+                {editingId && (
+                  <Button type="button" variant="outline" onClick={handleCancel}>
+                    Cancelar
+                  </Button>
+                )}
+              </div>
+            </form>
+
+            {error && (
+              <Alert className="mt-4 border-red-200 bg-red-50">
+                <AlertDescription className="text-red-800">{error}</AlertDescription>
+              </Alert>
+            )}
+
+            {success && (
+              <Alert className="mt-4 border-green-200 bg-green-50">
+                <AlertDescription className="text-green-800">{success}</AlertDescription>
+              </Alert>
+            )}
+          </CardContent>
+        </Card>
+
+        {/* Lista de Secciones */}
+        <Card>
+          <CardHeader>
+            <CardTitle>Secciones Registradas ({sections.length})</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-3 max-h-96 overflow-y-auto">
+              {sections.length === 0 ? (
+                <p className="text-gray-500 text-center py-4">No hay secciones registradas</p>
+              ) : (
+                sections.map((section) => (
+                  <div
+                    key={section.id}
+                    className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
+                  >
+                    <div className="flex-1">
+                      <div className="flex items-center gap-2">
+                        <h3 className="font-medium">{section.name}</h3>
+                        <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
+                          {section.classCode}
+                        </span>
+                      </div>
+                      <p className="text-sm text-gray-600 mt-1">
+                        {section.className} - {section.periodName}
+                      </p>
+                      <p className="text-xs text-gray-500 mt-1">
+                        Máx. estudiantes: {section.maxStudents} | Estado: {section.isActive ? 'Activa' : 'Inactiva'}
+                      </p>
+                    </div>
+                    <div className="flex gap-2">
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleEdit(section)}
+                      >
+                        <Edit className="h-4 w-4" />
+                      </Button>
+                      <Button
+                        size="sm"
+                        variant="outline"
+                        onClick={() => handleDelete(section.id)}
+                        className="text-red-600 hover:text-red-700"
+                      >
+                        <Trash2 className="h-4 w-4" />
+                      </Button>
+                    </div>
+                  </div>
+                ))
+              )}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 324 - 0
src/app/admin/students/page.tsx

@@ -0,0 +1,324 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Plus, Edit, Trash2, Users, ArrowLeft } from 'lucide-react';
+import Link from 'next/link';
+
+interface Student {
+  id: string;
+  firstName: string;
+  lastName: string;
+  email: string;
+  cedula: string;
+  phone: string;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface FormData {
+  firstName: string;
+  lastName: string;
+  email: string;
+  cedula: string;
+  phone: string;
+}
+
+export default function StudentsPage() {
+  const [students, setStudents] = useState<Student[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const [showForm, setShowForm] = useState(false);
+  const [editingStudent, setEditingStudent] = useState<Student | null>(null);
+  const [formData, setFormData] = useState<FormData>({
+    firstName: '',
+    lastName: '',
+    email: '',
+    cedula: '',
+    phone: '',
+  });
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+
+  useEffect(() => {
+    fetchStudents();
+  }, []);
+
+  const fetchStudents = async () => {
+    try {
+      const response = await fetch('/api/admin/students');
+      if (response.ok) {
+        const data = await response.json();
+        setStudents(data);
+      }
+    } catch (error) {
+      console.error('Error fetching students:', error);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError('');
+    setSuccess('');
+
+    try {
+      const url = editingStudent 
+        ? `/api/admin/students/${editingStudent.id}`
+        : '/api/admin/students';
+      const method = editingStudent ? 'PUT' : 'POST';
+
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        setSuccess(editingStudent ? 'Estudiante actualizado exitosamente' : 'Estudiante creado exitosamente');
+        setShowForm(false);
+        setEditingStudent(null);
+        setFormData({ firstName: '', lastName: '', email: '', cedula: '', phone: '' });
+        fetchStudents();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al procesar la solicitud');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleEdit = (student: Student) => {
+    setEditingStudent(student);
+    setFormData({
+      firstName: student.firstName,
+      lastName: student.lastName,
+      email: student.email,
+      cedula: student.cedula,
+      phone: student.phone,
+    });
+    setShowForm(true);
+    setError('');
+    setSuccess('');
+  };
+
+  const handleDelete = async (studentId: string) => {
+    if (!confirm('¿Estás seguro de que deseas eliminar este estudiante?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/students/${studentId}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        setSuccess('Estudiante eliminado exitosamente');
+        fetchStudents();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al eliminar el estudiante');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const resetForm = () => {
+    setShowForm(false);
+    setEditingStudent(null);
+    setFormData({ firstName: '', lastName: '', email: '', cedula: '', phone: '' });
+    setError('');
+    setSuccess('');
+  };
+
+  if (isLoading) {
+    return (
+      <div className="container mx-auto p-6">
+        <div className="flex items-center justify-center h-64">
+          <div className="text-lg">Cargando estudiantes...</div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="container mx-auto p-6">
+      <div className="flex items-center justify-between mb-6">
+        <div className="flex items-center gap-4">
+          <Link href="/admin/dashboard">
+            <Button variant="outline" size="sm">
+              <ArrowLeft className="h-4 w-4 mr-2" />
+              Volver al Dashboard
+            </Button>
+          </Link>
+          <div className="flex items-center gap-2">
+            <Users className="h-6 w-6" />
+            <h1 className="text-2xl font-bold">Gestión de Estudiantes</h1>
+          </div>
+        </div>
+        <Button onClick={() => setShowForm(true)} className="flex items-center gap-2">
+          <Plus className="h-4 w-4" />
+          Nuevo Estudiante
+        </Button>
+      </div>
+
+      {error && (
+        <Alert className="mb-4 border-red-200 bg-red-50">
+          <AlertDescription className="text-red-800">{error}</AlertDescription>
+        </Alert>
+      )}
+
+      {success && (
+        <Alert className="mb-4 border-green-200 bg-green-50">
+          <AlertDescription className="text-green-800">{success}</AlertDescription>
+        </Alert>
+      )}
+
+      {showForm && (
+        <Card className="mb-6">
+          <CardHeader>
+            <CardTitle>
+              {editingStudent ? 'Editar Estudiante' : 'Nuevo Estudiante'}
+            </CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleSubmit} className="space-y-4">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                <div>
+                  <Label htmlFor="firstName">Nombres</Label>
+                  <Input
+                    id="firstName"
+                    value={formData.firstName}
+                    onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
+                    required
+                  />
+                </div>
+                <div>
+                  <Label htmlFor="lastName">Apellidos</Label>
+                  <Input
+                    id="lastName"
+                    value={formData.lastName}
+                    onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
+                    required
+                  />
+                </div>
+                <div>
+                  <Label htmlFor="email">Correo Electrónico</Label>
+                  <Input
+                    id="email"
+                    type="email"
+                    value={formData.email}
+                    onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                    required
+                  />
+                </div>
+                <div>
+                  <Label htmlFor="cedula">Cédula</Label>
+                  <Input
+                    id="cedula"
+                    value={formData.cedula}
+                    onChange={(e) => setFormData({ ...formData, cedula: e.target.value })}
+                    required
+                  />
+                </div>
+                <div>
+                  <Label htmlFor="phone">Teléfono</Label>
+                  <Input
+                    id="phone"
+                    value={formData.phone}
+                    onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
+                    required
+                  />
+                </div>
+              </div>
+              <div className="flex gap-2">
+                <Button type="submit">
+                  {editingStudent ? 'Actualizar' : 'Crear'} Estudiante
+                </Button>
+                <Button type="button" variant="outline" onClick={resetForm}>
+                  Cancelar
+                </Button>
+              </div>
+            </form>
+          </CardContent>
+        </Card>
+      )}
+
+      <Card>
+        <CardHeader>
+          <CardTitle>Lista de Estudiantes ({students.length})</CardTitle>
+        </CardHeader>
+        <CardContent>
+          {students.length === 0 ? (
+            <div className="text-center py-8 text-gray-500">
+              No hay estudiantes registrados
+            </div>
+          ) : (
+            <div className="overflow-x-auto">
+              <table className="w-full border-collapse">
+                <thead>
+                  <tr className="border-b">
+                    <th className="text-left p-2">Nombre</th>
+                    <th className="text-left p-2">Email</th>
+                    <th className="text-left p-2">Cédula</th>
+                    <th className="text-left p-2">Teléfono</th>
+                    <th className="text-left p-2">Estado</th>
+                    <th className="text-left p-2">Acciones</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {students.map((student) => (
+                    <tr key={student.id} className="border-b hover:bg-gray-50">
+                      <td className="p-2">
+                        {student.firstName} {student.lastName}
+                      </td>
+                      <td className="p-2">{student.email}</td>
+                      <td className="p-2">{student.cedula}</td>
+                      <td className="p-2">{student.phone}</td>
+                      <td className="p-2">
+                        <span className={`px-2 py-1 rounded-full text-xs ${
+                          student.isActive 
+                            ? 'bg-green-100 text-green-800' 
+                            : 'bg-red-100 text-red-800'
+                        }`}>
+                          {student.isActive ? 'Activo' : 'Inactivo'}
+                        </span>
+                      </td>
+                      <td className="p-2">
+                        <div className="flex gap-2">
+                          <Button
+                            size="sm"
+                            variant="outline"
+                            onClick={() => handleEdit(student)}
+                          >
+                            <Edit className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            size="sm"
+                            variant="outline"
+                            onClick={() => handleDelete(student.id)}
+                            className="text-red-600 hover:text-red-700"
+                          >
+                            <Trash2 className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

+ 350 - 0
src/app/admin/teachers/page.tsx

@@ -0,0 +1,350 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Plus, Edit, Trash2, ArrowLeft } from 'lucide-react';
+import Link from 'next/link';
+
+interface Teacher {
+  id: string;
+  firstName: string;
+  lastName: string;
+  email: string;
+  cedula: string;
+  phone: string;
+  isActive: boolean;
+  createdAt: string;
+}
+
+interface TeacherFormData {
+  firstName: string;
+  lastName: string;
+  email: string;
+  cedula: string;
+  phone: string;
+  password?: string;
+}
+
+export default function TeachersPage() {
+  const [teachers, setTeachers] = useState<Teacher[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [showForm, setShowForm] = useState(false);
+  const [editingTeacher, setEditingTeacher] = useState<Teacher | null>(null);
+  const [formData, setFormData] = useState<TeacherFormData>({
+    firstName: '',
+    lastName: '',
+    email: '',
+    cedula: '',
+    phone: '',
+    password: '',
+  });
+  const [error, setError] = useState('');
+  const [success, setSuccess] = useState('');
+
+  useEffect(() => {
+    fetchTeachers();
+  }, []);
+
+  const fetchTeachers = async () => {
+    try {
+      const response = await fetch('/api/admin/teachers');
+      if (response.ok) {
+        const data = await response.json();
+        setTeachers(data);
+      }
+    } catch (error) {
+      console.error('Error fetching teachers:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError('');
+    setSuccess('');
+
+    try {
+      const url = editingTeacher 
+        ? `/api/admin/teachers/${editingTeacher.id}`
+        : '/api/admin/teachers';
+      
+      const method = editingTeacher ? 'PUT' : 'POST';
+      
+      const response = await fetch(url, {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(formData),
+      });
+
+      if (response.ok) {
+        setSuccess(editingTeacher ? 'Profesor actualizado exitosamente' : 'Profesor creado exitosamente');
+        resetForm();
+        fetchTeachers();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al procesar la solicitud');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const handleEdit = (teacher: Teacher) => {
+    setEditingTeacher(teacher);
+    setFormData({
+      firstName: teacher.firstName,
+      lastName: teacher.lastName,
+      email: teacher.email,
+      cedula: teacher.cedula,
+      phone: teacher.phone,
+    });
+    setShowForm(true);
+  };
+
+  const handleDelete = async (id: string) => {
+    if (!confirm('¿Está seguro de que desea eliminar este profesor?')) {
+      return;
+    }
+
+    try {
+      const response = await fetch(`/api/admin/teachers/${id}`, {
+        method: 'DELETE',
+      });
+
+      if (response.ok) {
+        setSuccess('Profesor eliminado exitosamente');
+        fetchTeachers();
+      } else {
+        const errorData = await response.json();
+        setError(errorData.error || 'Error al eliminar el profesor');
+      }
+    } catch (error) {
+      setError('Error de conexión');
+    }
+  };
+
+  const resetForm = () => {
+    setFormData({
+      firstName: '',
+      lastName: '',
+      email: '',
+      cedula: '',
+      phone: '',
+      password: '',
+    });
+    setEditingTeacher(null);
+    setShowForm(false);
+  };
+
+  if (loading) {
+    return (
+      <div className="min-h-screen flex items-center justify-center">
+        <div className="text-lg">Cargando profesores...</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="bg-white shadow">
+        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+          <div className="flex justify-between items-center py-6">
+            <div className="flex items-center space-x-4">
+              <Link href="/admin/dashboard">
+                <Button variant="outline" size="sm">
+                  <ArrowLeft className="h-4 w-4 mr-2" />
+                  Volver
+                </Button>
+              </Link>
+              <h1 className="text-3xl font-bold text-gray-900">
+                Gestión de Profesores
+              </h1>
+            </div>
+            <Button onClick={() => setShowForm(true)}>
+              <Plus className="h-4 w-4 mr-2" />
+              Nuevo Profesor
+            </Button>
+          </div>
+        </div>
+      </div>
+
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+        {error && (
+          <Alert variant="destructive" className="mb-6">
+            <AlertDescription>{error}</AlertDescription>
+          </Alert>
+        )}
+        
+        {success && (
+          <Alert className="mb-6">
+            <AlertDescription>{success}</AlertDescription>
+          </Alert>
+        )}
+
+        {showForm && (
+          <Card className="mb-8">
+            <CardHeader>
+              <CardTitle>
+                {editingTeacher ? 'Editar Profesor' : 'Nuevo Profesor'}
+              </CardTitle>
+              <CardDescription>
+                {editingTeacher 
+                  ? 'Modifica los datos del profesor'
+                  : 'Completa los datos para crear un nuevo profesor'
+                }
+              </CardDescription>
+            </CardHeader>
+            <CardContent>
+              <form onSubmit={handleSubmit} className="space-y-4">
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                  <div>
+                    <Label htmlFor="firstName">Nombre</Label>
+                    <Input
+                      id="firstName"
+                      value={formData.firstName}
+                      onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
+                      required
+                    />
+                  </div>
+                  <div>
+                    <Label htmlFor="lastName">Apellido</Label>
+                    <Input
+                      id="lastName"
+                      value={formData.lastName}
+                      onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
+                      required
+                    />
+                  </div>
+                  <div>
+                    <Label htmlFor="email">Correo Electrónico</Label>
+                    <Input
+                      id="email"
+                      type="email"
+                      value={formData.email}
+                      onChange={(e) => setFormData({ ...formData, email: e.target.value })}
+                      required
+                    />
+                  </div>
+                  <div>
+                    <Label htmlFor="cedula">Cédula de Identidad</Label>
+                    <Input
+                      id="cedula"
+                      value={formData.cedula}
+                      onChange={(e) => setFormData({ ...formData, cedula: e.target.value })}
+                      required
+                    />
+                  </div>
+                  <div>
+                    <Label htmlFor="phone">Teléfono</Label>
+                    <Input
+                      id="phone"
+                      value={formData.phone}
+                      onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
+                      required
+                    />
+                  </div>
+                  {!editingTeacher && (
+                    <div>
+                      <Label htmlFor="password">Contraseña</Label>
+                      <Input
+                        id="password"
+                        type="password"
+                        value={formData.password}
+                        onChange={(e) => setFormData({ ...formData, password: e.target.value })}
+                        required
+                      />
+                    </div>
+                  )}
+                </div>
+                <div className="flex space-x-4">
+                  <Button type="submit">
+                    {editingTeacher ? 'Actualizar' : 'Crear'} Profesor
+                  </Button>
+                  <Button type="button" variant="outline" onClick={resetForm}>
+                    Cancelar
+                  </Button>
+                </div>
+              </form>
+            </CardContent>
+          </Card>
+        )}
+
+        <Card>
+          <CardHeader>
+            <CardTitle>Lista de Profesores</CardTitle>
+            <CardDescription>
+              Total: {teachers.length} profesores registrados
+            </CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="overflow-x-auto">
+              <table className="w-full border-collapse">
+                <thead>
+                  <tr className="border-b">
+                    <th className="text-left p-4">Nombre</th>
+                    <th className="text-left p-4">Email</th>
+                    <th className="text-left p-4">Cédula</th>
+                    <th className="text-left p-4">Teléfono</th>
+                    <th className="text-left p-4">Estado</th>
+                    <th className="text-left p-4">Acciones</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {teachers.map((teacher) => (
+                    <tr key={teacher.id} className="border-b hover:bg-gray-50">
+                      <td className="p-4">
+                        {teacher.firstName} {teacher.lastName}
+                      </td>
+                      <td className="p-4">{teacher.email}</td>
+                      <td className="p-4">{teacher.cedula}</td>
+                      <td className="p-4">{teacher.phone}</td>
+                      <td className="p-4">
+                        <span className={`px-2 py-1 rounded-full text-xs ${
+                          teacher.isActive 
+                            ? 'bg-green-100 text-green-800' 
+                            : 'bg-red-100 text-red-800'
+                        }`}>
+                          {teacher.isActive ? 'Activo' : 'Inactivo'}
+                        </span>
+                      </td>
+                      <td className="p-4">
+                        <div className="flex space-x-2">
+                          <Button
+                            size="sm"
+                            variant="outline"
+                            onClick={() => handleEdit(teacher)}
+                          >
+                            <Edit className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            size="sm"
+                            variant="outline"
+                            onClick={() => handleDelete(teacher.id)}
+                          >
+                            <Trash2 className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+              {teachers.length === 0 && (
+                <div className="text-center py-8 text-gray-500">
+                  No hay profesores registrados
+                </div>
+              )}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+}

+ 163 - 0
src/app/api/admin/classes/[id]/route.ts

@@ -0,0 +1,163 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { classes, sections, eq, and, ne } from '@/lib/db/schema';
+
+interface RouteContext {
+  params: Promise<{ id: string }>;
+}
+
+export async function PUT(request: NextRequest, context: RouteContext) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = await context.params;
+    const { name, code, credits, description } = await request.json();
+
+    // Validaciones
+    if (!name || !code || !credits) {
+      return NextResponse.json(
+        { error: 'Nombre, código y créditos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    if (typeof credits !== 'number' || credits < 1) {
+      return NextResponse.json(
+        { error: 'Los créditos deben ser un número mayor a 0' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la clase existe
+    const existingClass = await db
+      .select()
+      .from(classes)
+      .where(eq(classes.id, id))
+      .limit(1);
+
+    if (existingClass.length === 0) {
+      return NextResponse.json(
+        { error: 'Clase no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si ya existe otra clase con el mismo código
+    const duplicateClass = await db
+      .select()
+      .from(classes)
+      .where(and(
+        eq(classes.code, code),
+        ne(classes.id, id)
+      ))
+      .limit(1);
+
+    if (duplicateClass.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe otra clase con este código' },
+        { status: 400 }
+      );
+    }
+
+    // Actualizar clase
+    const updatedClass = await db
+      .update(classes)
+      .set({
+        name,
+        code,
+        credits,
+        description: description || null,
+        updatedAt: new Date(),
+      })
+      .where(eq(classes.id, id))
+      .returning({
+        id: classes.id,
+        name: classes.name,
+        code: classes.code,
+        credits: classes.credits,
+        description: classes.description,
+        isActive: classes.isActive,
+        updatedAt: classes.updatedAt,
+      });
+
+    return NextResponse.json(updatedClass[0]);
+  } catch (error) {
+    console.error('Error updating class:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function DELETE(request: NextRequest, context: RouteContext) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = await context.params;
+
+    // Verificar si la clase existe
+    const existingClass = await db
+      .select()
+      .from(classes)
+      .where(eq(classes.id, id))
+      .limit(1);
+
+    if (existingClass.length === 0) {
+      return NextResponse.json(
+        { error: 'Clase no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si la clase tiene secciones asociadas
+    const associatedSections = await db
+      .select()
+      .from(sections)
+      .where(eq(sections.classId, id))
+      .limit(1);
+
+    if (associatedSections.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar la clase porque tiene secciones asociadas' },
+        { status: 400 }
+      );
+    }
+
+    // Eliminar clase (soft delete)
+    await db
+      .update(classes)
+      .set({
+        isActive: false,
+        updatedAt: new Date(),
+      })
+      .where(eq(classes.id, id));
+
+    return NextResponse.json(
+      { message: 'Clase eliminada exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting class:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 111 - 0
src/app/api/admin/classes/route.ts

@@ -0,0 +1,111 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { classes, eq } from '@/lib/db/schema';
+
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const allClasses = await db
+      .select({
+        id: classes.id,
+        name: classes.name,
+        code: classes.code,
+        credits: classes.credits,
+        description: classes.description,
+        isActive: classes.isActive,
+        createdAt: classes.createdAt,
+      })
+      .from(classes)
+      .orderBy(classes.name);
+
+    return NextResponse.json(allClasses);
+  } catch (error) {
+    console.error('Error fetching classes:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { name, code, credits, description } = await request.json();
+
+    // Validaciones
+    if (!name || !code || !credits) {
+      return NextResponse.json(
+        { error: 'Nombre, código y créditos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    if (typeof credits !== 'number' || credits < 1) {
+      return NextResponse.json(
+        { error: 'Los créditos deben ser un número mayor a 0' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe una clase con el mismo código
+    const existingClass = await db
+      .select()
+      .from(classes)
+      .where(eq(classes.code, code))
+      .limit(1);
+
+    if (existingClass.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe una clase con este código' },
+        { status: 400 }
+      );
+    }
+
+    // Crear clase
+    const [newClass] = await db
+      .insert(classes)
+      .values({
+        name,
+        code,
+        credits,
+        description: description || null,
+        isActive: true,
+      })
+      .returning({
+        id: classes.id,
+        name: classes.name,
+        code: classes.code,
+        credits: classes.credits,
+        description: classes.description,
+        isActive: classes.isActive,
+        createdAt: classes.createdAt,
+      });
+
+    return NextResponse.json(newClass, { status: 201 });
+  } catch (error) {
+    console.error('Error creating class:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 181 - 0
src/app/api/admin/partials/[id]/route.ts

@@ -0,0 +1,181 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { partials, periods, attendance, eq, and, ne } from '@/lib/db/schema';
+
+interface RouteContext {
+  params: Promise<{ id: string }>;
+}
+
+export async function PUT(request: NextRequest, context: RouteContext) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = await context.params;
+    const { name, periodId, startDate, endDate } = await request.json();
+
+    // Validaciones
+    if (!name || !periodId || !startDate || !endDate) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+
+    if (start >= end) {
+      return NextResponse.json(
+        { error: 'La fecha de inicio debe ser anterior a la fecha de fin' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el parcial existe
+    const existingPartial = await db
+      .select()
+      .from(partials)
+      .where(eq(partials.id, id))
+      .limit(1);
+
+    if (existingPartial.length === 0) {
+      return NextResponse.json(
+        { error: 'Parcial no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, periodId))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'El período seleccionado no existe' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe otro parcial con el mismo nombre en el mismo período
+    const duplicatePartial = await db
+      .select()
+      .from(partials)
+      .where(and(
+        eq(partials.name, name),
+        eq(partials.periodId, periodId),
+        ne(partials.id, id)
+      ))
+      .limit(1);
+
+    if (duplicatePartial.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe otro parcial con este nombre en el mismo período' },
+        { status: 400 }
+      );
+    }
+
+    // Actualizar parcial
+    const updatedPartial = await db
+      .update(partials)
+      .set({
+        name,
+        periodId,
+        startDate: start.toISOString().split('T')[0],
+        endDate: end.toISOString().split('T')[0],
+        updatedAt: new Date(),
+      })
+      .where(eq(partials.id, id))
+      .returning({
+        id: partials.id,
+        name: partials.name,
+        periodId: partials.periodId,
+        startDate: partials.startDate,
+        endDate: partials.endDate,
+        isActive: partials.isActive,
+        updatedAt: partials.updatedAt,
+      });
+
+    return NextResponse.json(updatedPartial[0]);
+  } catch (error) {
+    console.error('Error updating partial:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function DELETE(request: NextRequest, context: RouteContext) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = await context.params;
+
+    // Verificar si el parcial existe
+    const existingPartial = await db
+      .select()
+      .from(partials)
+      .where(eq(partials.id, id))
+      .limit(1);
+
+    if (existingPartial.length === 0) {
+      return NextResponse.json(
+        { error: 'Parcial no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el parcial tiene registros de asistencia asociados
+    const associatedAttendance = await db
+      .select()
+      .from(attendance)
+      .where(eq(attendance.partialId, id))
+      .limit(1);
+
+    if (associatedAttendance.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar el parcial porque tiene registros de asistencia asociados' },
+        { status: 400 }
+      );
+    }
+
+    // Eliminar parcial (soft delete)
+    await db
+      .update(partials)
+      .set({
+        isActive: false,
+        updatedAt: new Date(),
+      })
+      .where(eq(partials.id, id));
+
+    return NextResponse.json(
+      { message: 'Parcial eliminado exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting partial:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 133 - 0
src/app/api/admin/partials/route.ts

@@ -0,0 +1,133 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { partials, periods, eq, and } from '@/lib/db/schema';
+
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const allPartials = await db
+      .select({
+        id: partials.id,
+        name: partials.name,
+        periodId: partials.periodId,
+        periodName: periods.name,
+        startDate: partials.startDate,
+        endDate: partials.endDate,
+        isActive: partials.isActive,
+        createdAt: partials.createdAt,
+      })
+      .from(partials)
+      .leftJoin(periods, eq(partials.periodId, periods.id))
+      .orderBy(periods.name, partials.name);
+
+    return NextResponse.json(allPartials);
+  } catch (error) {
+    console.error('Error fetching partials:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { name, periodId, startDate, endDate } = await request.json();
+
+    // Validaciones
+    if (!name || !periodId || !startDate || !endDate) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+
+    if (start >= end) {
+      return NextResponse.json(
+        { error: 'La fecha de inicio debe ser anterior a la fecha de fin' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, periodId))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'El período seleccionado no existe' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe un parcial con el mismo nombre en el mismo período
+    const existingPartial = await db
+      .select()
+      .from(partials)
+      .where(and(
+        eq(partials.name, name),
+        eq(partials.periodId, periodId)
+      ))
+      .limit(1);
+
+    if (existingPartial.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe un parcial con este nombre en el mismo período' },
+        { status: 400 }
+      );
+    }
+
+    // Crear parcial
+    const [newPartial] = await db
+      .insert(partials)
+      .values({
+        name,
+        periodId,
+        startDate: start.toISOString().split('T')[0],
+        endDate: end.toISOString().split('T')[0],
+        isActive: true,
+      })
+      .returning({
+        id: partials.id,
+        name: partials.name,
+        periodId: partials.periodId,
+        startDate: partials.startDate,
+        endDate: partials.endDate,
+        isActive: partials.isActive,
+        createdAt: partials.createdAt,
+      });
+
+    return NextResponse.json(newPartial, { status: 201 });
+  } catch (error) {
+    console.error('Error creating partial:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 176 - 0
src/app/api/admin/periods/[id]/route.ts

@@ -0,0 +1,176 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { periods, sections, partials } from '@/lib/db/schema';
+import { eq, and } from 'drizzle-orm';
+
+// PUT - Actualizar período académico
+export async function PUT(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { name, startDate, endDate } = body;
+
+    // Validar campos requeridos
+    if (!name || !startDate || !endDate) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Validar fechas
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+    
+    if (start >= end) {
+      return NextResponse.json(
+        { error: 'La fecha de inicio debe ser anterior a la fecha de fin' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, id))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'Período no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el nombre ya existe (excluyendo el período actual)
+    const existingName = await db
+      .select()
+      .from(periods)
+      .where(and(eq(periods.name, name), eq(periods.id, id)))
+      .limit(1);
+
+    if (existingName.length === 0) {
+      const nameCheck = await db
+        .select()
+        .from(periods)
+        .where(eq(periods.name, name))
+        .limit(1);
+
+      if (nameCheck.length > 0) {
+        return NextResponse.json(
+          { error: 'Ya existe un período con este nombre' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar período
+    const updatedPeriod = await db
+      .update(periods)
+      .set({
+        name,
+        startDate: start.toISOString().split('T')[0],
+        endDate: end.toISOString().split('T')[0],
+        updatedAt: new Date(),
+      })
+      .where(eq(periods.id, id))
+      .returning({
+        id: periods.id,
+        name: periods.name,
+        startDate: periods.startDate,
+        endDate: periods.endDate,
+        isActive: periods.isActive,
+        createdAt: periods.createdAt,
+      });
+
+    return NextResponse.json(updatedPeriod);
+  } catch (error) {
+    console.error('Error updating period:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar período académico
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, id))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'Período no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el período tiene secciones asociadas (las secciones están vinculadas a períodos)
+    const associatedSections = await db
+      .select()
+      .from(sections)
+      .where(eq(sections.periodId, id))
+      .limit(1);
+
+    if (associatedSections.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar el período porque tiene secciones asociadas' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el período tiene parciales asociados
+    const associatedPartials = await db
+      .select()
+      .from(partials)
+      .where(eq(partials.periodId, id))
+      .limit(1);
+
+    if (associatedPartials.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar el período porque tiene parciales asociados' },
+        { status: 400 }
+      );
+    }
+
+    // Eliminar período
+    await db
+      .delete(periods)
+      .where(eq(periods.id, id));
+
+    return NextResponse.json({ message: 'Período eliminado exitosamente' });
+  } catch (error) {
+    console.error('Error deleting period:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 70 - 0
src/app/api/admin/periods/[id]/toggle/route.ts

@@ -0,0 +1,70 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { periods } from '@/lib/db/schema';
+import { eq } from 'drizzle-orm';
+
+// PATCH - Activar/Desactivar período académico
+export async function PATCH(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { isActive } = body;
+
+    if (typeof isActive !== 'boolean') {
+      return NextResponse.json(
+        { error: 'El campo isActive debe ser un valor booleano' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, id))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'Período no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Actualizar estado del período
+    const [updatedPeriod] = await db
+      .update(periods)
+      .set({
+        isActive,
+        updatedAt: new Date(),
+      })
+      .where(eq(periods.id, id))
+      .returning({
+        id: periods.id,
+        name: periods.name,
+        startDate: periods.startDate,
+        endDate: periods.endDate,
+        isActive: periods.isActive,
+        createdAt: periods.createdAt,
+      });
+
+    return NextResponse.json(updatedPeriod);
+  } catch (error) {
+    console.error('Error toggling period status:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 110 - 0
src/app/api/admin/periods/route.ts

@@ -0,0 +1,110 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { periods } from '@/lib/db/schema';
+import { eq } from 'drizzle-orm';
+
+// GET - Obtener todos los períodos académicos
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const allPeriods = await db
+      .select({
+        id: periods.id,
+        name: periods.name,
+        startDate: periods.startDate,
+        endDate: periods.endDate,
+        isActive: periods.isActive,
+        createdAt: periods.createdAt,
+      })
+      .from(periods)
+      .orderBy(periods.startDate);
+
+    return NextResponse.json(allPeriods);
+  } catch (error) {
+    console.error('Error fetching periods:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nuevo período académico
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { name, startDate, endDate } = body;
+
+    // Validar campos requeridos
+    if (!name || !startDate || !endDate) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Validar fechas
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+    
+    if (start >= end) {
+      return NextResponse.json(
+        { error: 'La fecha de inicio debe ser anterior a la fecha de fin' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el nombre ya existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.name, name))
+      .limit(1);
+
+    if (existingPeriod.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe un período con este nombre' },
+        { status: 400 }
+      );
+    }
+
+    // Crear período académico
+    const [newPeriod] = await db
+      .insert(periods)
+      .values({
+        name,
+        startDate: start.toISOString().split('T')[0],
+        endDate: end.toISOString().split('T')[0],
+        isActive: true,
+      })
+      .returning({
+        id: periods.id,
+        name: periods.name,
+        startDate: periods.startDate,
+        endDate: periods.endDate,
+        isActive: periods.isActive,
+        createdAt: periods.createdAt,
+      });
+
+    return NextResponse.json(newPeriod, { status: 201 });
+  } catch (error) {
+    console.error('Error creating period:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 206 - 0
src/app/api/admin/sections/[id]/route.ts

@@ -0,0 +1,206 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { sections, classes, periods, studentEnrollments, eq, and, ne } from '@/lib/db/schema';
+
+interface RouteContext {
+  params: Promise<{ id: string }>;
+}
+
+export async function PUT(request: NextRequest, context: RouteContext) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = await context.params;
+    const { name, classId, periodId, maxStudents } = await request.json();
+
+    // Validaciones
+    if (!name || !classId || !periodId || !maxStudents) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    if (typeof maxStudents !== 'number' || maxStudents < 1) {
+      return NextResponse.json(
+        { error: 'El número máximo de estudiantes debe ser mayor a 0' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la sección existe
+    const existingSection = await db
+      .select()
+      .from(sections)
+      .where(eq(sections.id, id))
+      .limit(1);
+
+    if (existingSection.length === 0) {
+      return NextResponse.json(
+        { error: 'Sección no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si la clase existe
+    const existingClass = await db
+      .select()
+      .from(classes)
+      .where(eq(classes.id, classId))
+      .limit(1);
+
+    if (existingClass.length === 0) {
+      return NextResponse.json(
+        { error: 'La clase seleccionada no existe' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, periodId))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'El período seleccionado no existe' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe otra sección con el mismo nombre para la misma clase y período
+    const duplicateSection = await db
+      .select()
+      .from(sections)
+      .where(and(
+        eq(sections.name, name),
+        eq(sections.classId, classId),
+        eq(sections.periodId, periodId),
+        ne(sections.id, id)
+      ))
+      .limit(1);
+
+    if (duplicateSection.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe otra sección con este nombre para la misma clase y período' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el nuevo límite de estudiantes es menor al número actual de estudiantes inscritos
+    const currentEnrollments = await db
+      .select()
+      .from(studentEnrollments)
+      .where(eq(studentEnrollments.sectionId, id));
+
+    if (maxStudents < currentEnrollments.length) {
+      return NextResponse.json(
+        { error: `No se puede reducir el límite a ${maxStudents} porque hay ${currentEnrollments.length} estudiantes inscritos` },
+        { status: 400 }
+      );
+    }
+
+    // Actualizar sección
+    const updatedSection = await db
+      .update(sections)
+      .set({
+        name,
+        classId,
+        periodId,
+        maxStudents,
+        updatedAt: new Date(),
+      })
+      .where(eq(sections.id, id))
+      .returning({
+        id: sections.id,
+        name: sections.name,
+        classId: sections.classId,
+        periodId: sections.periodId,
+        maxStudents: sections.maxStudents,
+        isActive: sections.isActive,
+        updatedAt: sections.updatedAt,
+      });
+
+    return NextResponse.json(updatedSection[0]);
+  } catch (error) {
+    console.error('Error updating section:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function DELETE(request: NextRequest, context: RouteContext) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { id } = await context.params;
+
+    // Verificar si la sección existe
+    const existingSection = await db
+      .select()
+      .from(sections)
+      .where(eq(sections.id, id))
+      .limit(1);
+
+    if (existingSection.length === 0) {
+      return NextResponse.json(
+        { error: 'Sección no encontrada' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si la sección tiene estudiantes inscritos
+    const enrolledStudents = await db
+      .select()
+      .from(studentEnrollments)
+      .where(eq(studentEnrollments.sectionId, id))
+      .limit(1);
+
+    if (enrolledStudents.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar la sección porque tiene estudiantes inscritos' },
+        { status: 400 }
+      );
+    }
+
+    // Eliminar sección (soft delete)
+    await db
+      .update(sections)
+      .set({
+        isActive: false,
+        updatedAt: new Date(),
+      })
+      .where(eq(sections.id, id));
+
+    return NextResponse.json(
+      { message: 'Sección eliminada exitosamente' },
+      { status: 200 }
+    );
+  } catch (error) {
+    console.error('Error deleting section:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 148 - 0
src/app/api/admin/sections/route.ts

@@ -0,0 +1,148 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { sections, classes, periods, eq, and } from '@/lib/db/schema';
+
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const allSections = await db
+      .select({
+        id: sections.id,
+        name: sections.name,
+        classId: sections.classId,
+        className: classes.name,
+        classCode: classes.code,
+        periodId: sections.periodId,
+        periodName: periods.name,
+        maxStudents: sections.maxStudents,
+        isActive: sections.isActive,
+        createdAt: sections.createdAt,
+      })
+      .from(sections)
+      .leftJoin(classes, eq(sections.classId, classes.id))
+      .leftJoin(periods, eq(sections.periodId, periods.id))
+      .orderBy(classes.code, sections.name);
+
+    return NextResponse.json(allSections);
+  } catch (error) {
+    console.error('Error fetching sections:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session?.user || session.user.role !== 'admin') {
+      return NextResponse.json(
+        { error: 'No autorizado' },
+        { status: 401 }
+      );
+    }
+
+    const { name, classId, periodId, maxStudents } = await request.json();
+
+    // Validaciones
+    if (!name || !classId || !periodId || !maxStudents) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    if (typeof maxStudents !== 'number' || maxStudents < 1) {
+      return NextResponse.json(
+        { error: 'El número máximo de estudiantes debe ser mayor a 0' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la clase existe
+    const existingClass = await db
+      .select()
+      .from(classes)
+      .where(eq(classes.id, classId))
+      .limit(1);
+
+    if (existingClass.length === 0) {
+      return NextResponse.json(
+        { error: 'La clase seleccionada no existe' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el período existe
+    const existingPeriod = await db
+      .select()
+      .from(periods)
+      .where(eq(periods.id, periodId))
+      .limit(1);
+
+    if (existingPeriod.length === 0) {
+      return NextResponse.json(
+        { error: 'El período seleccionado no existe' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si ya existe una sección con el mismo nombre para la misma clase y período
+    const existingSection = await db
+      .select()
+      .from(sections)
+      .where(and(
+        eq(sections.name, name),
+        eq(sections.classId, classId),
+        eq(sections.periodId, periodId)
+      ))
+      .limit(1);
+
+    if (existingSection.length > 0) {
+      return NextResponse.json(
+        { error: 'Ya existe una sección con este nombre para la misma clase y período' },
+        { status: 400 }
+      );
+    }
+
+    // Crear sección
+    const [newSection] = await db
+      .insert(sections)
+      .values({
+        name,
+        classId,
+        periodId,
+        maxStudents,
+        isActive: true,
+      })
+      .returning({
+        id: sections.id,
+        name: sections.name,
+        classId: sections.classId,
+        periodId: sections.periodId,
+        maxStudents: sections.maxStudents,
+        isActive: sections.isActive,
+        createdAt: sections.createdAt,
+      });
+
+    return NextResponse.json(newSection, { status: 201 });
+  } catch (error) {
+    console.error('Error creating section:', error);
+    return NextResponse.json(
+      { error: 'Error interno del servidor' },
+      { status: 500 }
+    );
+  }
+}

+ 69 - 0
src/app/api/admin/stats/route.ts

@@ -0,0 +1,69 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { users, classes, sections, periods, partials } from '@/lib/db/schema';
+import { eq, count } from 'drizzle-orm';
+
+export async function GET(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    // Contar profesores
+    const [teachersCount] = await db
+      .select({ count: count() })
+      .from(users)
+      .where(eq(users.role, 'teacher'));
+
+    // Contar estudiantes
+    const [studentsCount] = await db
+      .select({ count: count() })
+      .from(users)
+      .where(eq(users.role, 'student'));
+
+    // Contar clases
+    const [classesCount] = await db
+      .select({ count: count() })
+      .from(classes)
+      .where(eq(classes.isActive, true));
+
+    // Contar secciones
+    const [sectionsCount] = await db
+      .select({ count: count() })
+      .from(sections)
+      .where(eq(sections.isActive, true));
+
+    // Contar periodos
+    const [periodsCount] = await db
+      .select({ count: count() })
+      .from(periods)
+      .where(eq(periods.isActive, true));
+
+    // Contar parciales
+    const [partialsCount] = await db
+      .select({ count: count() })
+      .from(partials)
+      .where(eq(partials.isActive, true));
+
+    const stats = {
+      teachers: teachersCount.count,
+      students: studentsCount.count,
+      classes: classesCount.count,
+      sections: sectionsCount.count,
+      periods: periodsCount.count,
+      partials: partialsCount.count,
+    };
+
+    return NextResponse.json(stats);
+  } catch (error) {
+    console.error('Error fetching admin stats:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 184 - 0
src/app/api/admin/students/[id]/route.ts

@@ -0,0 +1,184 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { users, studentEnrollments } from '@/lib/db/schema';
+import { eq, and } from 'drizzle-orm';
+
+// PUT - Actualizar estudiante
+export async function PUT(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { firstName, lastName, email, cedula, phone } = body;
+
+    // Validar campos requeridos
+    if (!firstName || !lastName || !email || !cedula || !phone) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el estudiante existe
+    const existingStudent = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, id), eq(users.role, 'student')))
+      .limit(1);
+
+    if (existingStudent.length === 0) {
+      return NextResponse.json(
+        { error: 'Estudiante no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el email ya existe (excluyendo el estudiante actual)
+    const existingEmail = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.email, email), eq(users.id, id)))
+      .limit(1);
+
+    if (existingEmail.length === 0) {
+      const emailCheck = await db
+        .select()
+        .from(users)
+        .where(eq(users.email, email))
+        .limit(1);
+
+      if (emailCheck.length > 0) {
+        return NextResponse.json(
+          { error: 'El correo electrónico ya está registrado' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Verificar si la cédula ya existe (excluyendo el estudiante actual)
+    const existingCedula = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.cedula, cedula), eq(users.id, id)))
+      .limit(1);
+
+    if (existingCedula.length === 0) {
+      const cedulaCheck = await db
+        .select()
+        .from(users)
+        .where(eq(users.cedula, cedula))
+        .limit(1);
+
+      if (cedulaCheck.length > 0) {
+        return NextResponse.json(
+          { error: 'La cédula ya está registrada' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar estudiante
+    const [updatedStudent] = await db
+      .update(users)
+      .set({
+        firstName,
+        lastName,
+        email,
+        cedula,
+        phone,
+        updatedAt: new Date(),
+      })
+      .where(eq(users.id, id))
+      .returning({
+        id: users.id,
+        firstName: users.firstName,
+        lastName: users.lastName,
+        email: users.email,
+        cedula: users.cedula,
+        phone: users.phone,
+        isActive: users.isActive,
+        createdAt: users.createdAt,
+      });
+
+    return NextResponse.json(updatedStudent);
+  } catch (error) {
+    console.error('Error updating student:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar estudiante (soft delete)
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    // Verificar si el estudiante existe
+    const existingStudent = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, id), eq(users.role, 'student')))
+      .limit(1);
+
+    if (existingStudent.length === 0) {
+      return NextResponse.json(
+        { error: 'Estudiante no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el estudiante tiene inscripciones activas
+    const activeEnrollments = await db
+      .select()
+      .from(studentEnrollments)
+      .where(and(
+        eq(studentEnrollments.studentId, id),
+        eq(studentEnrollments.isActive, true)
+      ))
+      .limit(1);
+
+    if (activeEnrollments.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar el estudiante porque tiene inscripciones activas' },
+        { status: 400 }
+      );
+    }
+
+    // Soft delete - marcar como inactivo
+    await db
+      .update(users)
+      .set({
+        isActive: false,
+        updatedAt: new Date(),
+      })
+      .where(eq(users.id, id));
+
+    return NextResponse.json({ message: 'Estudiante eliminado exitosamente' });
+  } catch (error) {
+    console.error('Error deleting student:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 126 - 0
src/app/api/admin/students/route.ts

@@ -0,0 +1,126 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { users } from '@/lib/db/schema';
+import { eq } from 'drizzle-orm';
+import bcrypt from 'bcryptjs';
+
+// GET - Obtener todos los estudiantes
+export async function GET() {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const students = await db
+      .select({
+        id: users.id,
+        firstName: users.firstName,
+        lastName: users.lastName,
+        email: users.email,
+        cedula: users.cedula,
+        phone: users.phone,
+        isActive: users.isActive,
+        createdAt: users.createdAt,
+      })
+      .from(users)
+      .where(eq(users.role, 'student'))
+      .orderBy(users.firstName, users.lastName);
+
+    return NextResponse.json(students);
+  } catch (error) {
+    console.error('Error fetching students:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nuevo estudiante
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { firstName, lastName, email, cedula, phone } = body;
+
+    // Validar campos requeridos
+    if (!firstName || !lastName || !email || !cedula || !phone) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el email ya existe
+    const existingEmail = await db
+      .select()
+      .from(users)
+      .where(eq(users.email, email))
+      .limit(1);
+
+    if (existingEmail.length > 0) {
+      return NextResponse.json(
+        { error: 'El correo electrónico ya está registrado' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la cédula ya existe
+    const existingCedula = await db
+      .select()
+      .from(users)
+      .where(eq(users.cedula, cedula))
+      .limit(1);
+
+    if (existingCedula.length > 0) {
+      return NextResponse.json(
+        { error: 'La cédula ya está registrada' },
+        { status: 400 }
+      );
+    }
+
+    // Generar contraseña temporal (cédula)
+    const hashedPassword = await bcrypt.hash(cedula, 10);
+
+    // Crear estudiante
+    const [newStudent] = await db
+      .insert(users)
+      .values({
+        firstName,
+        lastName,
+        email,
+        cedula,
+        phone,
+        password: hashedPassword,
+        role: 'student',
+        isActive: true,
+      })
+      .returning({
+        id: users.id,
+        firstName: users.firstName,
+        lastName: users.lastName,
+        email: users.email,
+        cedula: users.cedula,
+        phone: users.phone,
+        isActive: users.isActive,
+        createdAt: users.createdAt,
+      });
+
+    return NextResponse.json(newStudent, { status: 201 });
+  } catch (error) {
+    console.error('Error creating student:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 184 - 0
src/app/api/admin/teachers/[id]/route.ts

@@ -0,0 +1,184 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { users, teacherAssignments } from '@/lib/db/schema';
+import { eq, and } from 'drizzle-orm';
+
+// PUT - Actualizar profesor
+export async function PUT(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { firstName, lastName, email, cedula, phone } = body;
+
+    // Validar campos requeridos
+    if (!firstName || !lastName || !email || !cedula || !phone) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el profesor existe
+    const existingTeacher = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, id), eq(users.role, 'teacher')))
+      .limit(1);
+
+    if (existingTeacher.length === 0) {
+      return NextResponse.json(
+        { error: 'Profesor no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el email ya existe (excluyendo el profesor actual)
+    const existingEmail = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.email, email), eq(users.id, id)))
+      .limit(1);
+
+    if (existingEmail.length === 0) {
+      const emailCheck = await db
+        .select()
+        .from(users)
+        .where(eq(users.email, email))
+        .limit(1);
+
+      if (emailCheck.length > 0) {
+        return NextResponse.json(
+          { error: 'El correo electrónico ya está registrado' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Verificar si la cédula ya existe (excluyendo el profesor actual)
+    const existingCedula = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.cedula, cedula), eq(users.id, id)))
+      .limit(1);
+
+    if (existingCedula.length === 0) {
+      const cedulaCheck = await db
+        .select()
+        .from(users)
+        .where(eq(users.cedula, cedula))
+        .limit(1);
+
+      if (cedulaCheck.length > 0) {
+        return NextResponse.json(
+          { error: 'La cédula ya está registrada' },
+          { status: 400 }
+        );
+      }
+    }
+
+    // Actualizar profesor
+    const [updatedTeacher] = await db
+      .update(users)
+      .set({
+        firstName,
+        lastName,
+        email,
+        cedula,
+        phone,
+        updatedAt: new Date(),
+      })
+      .where(eq(users.id, id))
+      .returning({
+        id: users.id,
+        firstName: users.firstName,
+        lastName: users.lastName,
+        email: users.email,
+        cedula: users.cedula,
+        phone: users.phone,
+        isActive: users.isActive,
+        createdAt: users.createdAt,
+      });
+
+    return NextResponse.json(updatedTeacher);
+  } catch (error) {
+    console.error('Error updating teacher:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}
+
+// DELETE - Eliminar profesor (soft delete)
+export async function DELETE(
+  request: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    // Verificar si el profesor existe
+    const existingTeacher = await db
+      .select()
+      .from(users)
+      .where(and(eq(users.id, id), eq(users.role, 'teacher')))
+      .limit(1);
+
+    if (existingTeacher.length === 0) {
+      return NextResponse.json(
+        { error: 'Profesor no encontrado' },
+        { status: 404 }
+      );
+    }
+
+    // Verificar si el profesor tiene asignaciones activas
+    const activeAssignments = await db
+      .select()
+      .from(teacherAssignments)
+      .where(and(
+        eq(teacherAssignments.teacherId, id),
+        eq(teacherAssignments.isActive, true)
+      ))
+      .limit(1);
+
+    if (activeAssignments.length > 0) {
+      return NextResponse.json(
+        { error: 'No se puede eliminar el profesor porque tiene asignaciones activas' },
+        { status: 400 }
+      );
+    }
+
+    // Soft delete - marcar como inactivo
+    await db
+      .update(users)
+      .set({
+        isActive: false,
+        updatedAt: new Date(),
+      })
+      .where(eq(users.id, id));
+
+    return NextResponse.json({ message: 'Profesor eliminado exitosamente' });
+  } catch (error) {
+    console.error('Error deleting teacher:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 125 - 0
src/app/api/admin/teachers/route.ts

@@ -0,0 +1,125 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/lib/auth';
+import { db } from '@/lib/db';
+import { users } from '@/lib/db/schema';
+import { eq, and } from 'drizzle-orm';
+import bcrypt from 'bcryptjs';
+
+// GET - Obtener todos los profesores
+export async function GET(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const teachers = await db
+      .select({
+        id: users.id,
+        firstName: users.firstName,
+        lastName: users.lastName,
+        email: users.email,
+        cedula: users.cedula,
+        phone: users.phone,
+        isActive: users.isActive,
+        createdAt: users.createdAt,
+      })
+      .from(users)
+      .where(eq(users.role, 'teacher'))
+      .orderBy(users.firstName);
+
+    return NextResponse.json(teachers);
+  } catch (error) {
+    console.error('Error fetching teachers:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}
+
+// POST - Crear nuevo profesor
+export async function POST(request: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions);
+    
+    if (!session || session.user.role !== 'admin') {
+      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+    }
+
+    const body = await request.json();
+    const { firstName, lastName, email, cedula, phone, password } = body;
+
+    // Validar campos requeridos
+    if (!firstName || !lastName || !email || !cedula || !phone || !password) {
+      return NextResponse.json(
+        { error: 'Todos los campos son requeridos' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si el email ya existe
+    const existingUser = await db
+      .select()
+      .from(users)
+      .where(eq(users.email, email))
+      .limit(1);
+
+    if (existingUser.length > 0) {
+      return NextResponse.json(
+        { error: 'El correo electrónico ya está registrado' },
+        { status: 400 }
+      );
+    }
+
+    // Verificar si la cédula ya existe
+    const existingCedula = await db
+      .select()
+      .from(users)
+      .where(eq(users.cedula, cedula))
+      .limit(1);
+
+    if (existingCedula.length > 0) {
+      return NextResponse.json(
+        { error: 'La cédula ya está registrada' },
+        { status: 400 }
+      );
+    }
+
+    // Encriptar contraseña
+    const hashedPassword = await bcrypt.hash(password, 10);
+
+    // Crear profesor
+    const [newTeacher] = await db
+      .insert(users)
+      .values({
+        firstName,
+        lastName,
+        email,
+        cedula,
+        phone,
+        password: hashedPassword,
+        role: 'teacher',
+      })
+      .returning({
+        id: users.id,
+        firstName: users.firstName,
+        lastName: users.lastName,
+        email: users.email,
+        cedula: users.cedula,
+        phone: users.phone,
+        isActive: users.isActive,
+        createdAt: users.createdAt,
+      });
+
+    return NextResponse.json(newTeacher, { status: 201 });
+  } catch (error) {
+    console.error('Error creating teacher:', error);
+    return NextResponse.json(
+      { error: 'Internal server error' },
+      { status: 500 }
+    );
+  }
+}

+ 6 - 0
src/app/api/auth/[...nextauth]/route.ts

@@ -0,0 +1,6 @@
+import NextAuth from 'next-auth';
+import { authOptions } from '@/lib/auth';
+
+const handler = NextAuth(authOptions);
+
+export { handler as GET, handler as POST };

+ 99 - 0
src/app/auth/signin/page.tsx

@@ -0,0 +1,99 @@
+'use client';
+
+import { useState } from 'react';
+import { signIn, getSession } from 'next-auth/react';
+import { useRouter } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+
+export default function SignIn() {
+  const [email, setEmail] = useState('');
+  const [password, setPassword] = useState('');
+  const [error, setError] = useState('');
+  const [loading, setLoading] = useState(false);
+  const router = useRouter();
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    setError('');
+
+    try {
+      const result = await signIn('credentials', {
+        email,
+        password,
+        redirect: false,
+      });
+
+      if (result?.error) {
+        setError('Credenciales inválidas');
+      } else {
+        const session = await getSession();
+        if (session?.user?.role === 'admin') {
+          router.push('/admin/dashboard');
+        } else if (session?.user?.role === 'teacher') {
+          router.push('/teacher/dashboard');
+        } else if (session?.user?.role === 'student') {
+          router.push('/student/dashboard');
+        } else {
+          router.push('/');
+        }
+      }
+    } catch (error) {
+      setError('Error al iniciar sesión');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="min-h-screen flex items-center justify-center bg-gray-50">
+      <Card className="w-full max-w-md">
+        <CardHeader className="space-y-1">
+          <CardTitle className="text-2xl font-bold text-center">
+            Iniciar Sesión
+          </CardTitle>
+          <CardDescription className="text-center">
+            Sistema de Gestión de Asistencia
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <form onSubmit={handleSubmit} className="space-y-4">
+            <div className="space-y-2">
+              <Label htmlFor="email">Correo Electrónico</Label>
+              <Input
+                id="email"
+                type="email"
+                placeholder="correo@ejemplo.com"
+                value={email}
+                onChange={(e) => setEmail(e.target.value)}
+                required
+              />
+            </div>
+            <div className="space-y-2">
+              <Label htmlFor="password">Contraseña</Label>
+              <Input
+                id="password"
+                type="password"
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                required
+              />
+            </div>
+            {error && (
+              <Alert variant="destructive">
+                <AlertDescription>{error}</AlertDescription>
+              </Alert>
+            )}
+            <Button type="submit" className="w-full" disabled={loading}>
+              {loading ? 'Iniciando sesión...' : 'Iniciar Sesión'}
+            </Button>
+          </form>
+        </CardContent>
+      </Card>
+    </div>
+  );
+}

+ 109 - 13
src/app/globals.css

@@ -1,26 +1,122 @@
 @import "tailwindcss";
+@import "tw-animate-css";
 
-:root {
-  --background: #ffffff;
-  --foreground: #171717;
-}
+@custom-variant dark (&:is(.dark *));
 
 @theme inline {
   --color-background: var(--background);
   --color-foreground: var(--foreground);
   --font-sans: var(--font-geist-sans);
   --font-mono: var(--font-geist-mono);
+  --color-sidebar-ring: var(--sidebar-ring);
+  --color-sidebar-border: var(--sidebar-border);
+  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+  --color-sidebar-accent: var(--sidebar-accent);
+  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+  --color-sidebar-primary: var(--sidebar-primary);
+  --color-sidebar-foreground: var(--sidebar-foreground);
+  --color-sidebar: var(--sidebar);
+  --color-chart-5: var(--chart-5);
+  --color-chart-4: var(--chart-4);
+  --color-chart-3: var(--chart-3);
+  --color-chart-2: var(--chart-2);
+  --color-chart-1: var(--chart-1);
+  --color-ring: var(--ring);
+  --color-input: var(--input);
+  --color-border: var(--border);
+  --color-destructive: var(--destructive);
+  --color-accent-foreground: var(--accent-foreground);
+  --color-accent: var(--accent);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-muted: var(--muted);
+  --color-secondary-foreground: var(--secondary-foreground);
+  --color-secondary: var(--secondary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-primary: var(--primary);
+  --color-popover-foreground: var(--popover-foreground);
+  --color-popover: var(--popover);
+  --color-card-foreground: var(--card-foreground);
+  --color-card: var(--card);
+  --radius-sm: calc(var(--radius) - 4px);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-lg: var(--radius);
+  --radius-xl: calc(var(--radius) + 4px);
 }
 
-@media (prefers-color-scheme: dark) {
-  :root {
-    --background: #0a0a0a;
-    --foreground: #ededed;
-  }
+:root {
+  --radius: 0.625rem;
+  --background: oklch(1 0 0);
+  --foreground: oklch(0.145 0 0);
+  --card: oklch(1 0 0);
+  --card-foreground: oklch(0.145 0 0);
+  --popover: oklch(1 0 0);
+  --popover-foreground: oklch(0.145 0 0);
+  --primary: oklch(0.205 0 0);
+  --primary-foreground: oklch(0.985 0 0);
+  --secondary: oklch(0.97 0 0);
+  --secondary-foreground: oklch(0.205 0 0);
+  --muted: oklch(0.97 0 0);
+  --muted-foreground: oklch(0.556 0 0);
+  --accent: oklch(0.97 0 0);
+  --accent-foreground: oklch(0.205 0 0);
+  --destructive: oklch(0.577 0.245 27.325);
+  --border: oklch(0.922 0 0);
+  --input: oklch(0.922 0 0);
+  --ring: oklch(0.708 0 0);
+  --chart-1: oklch(0.646 0.222 41.116);
+  --chart-2: oklch(0.6 0.118 184.704);
+  --chart-3: oklch(0.398 0.07 227.392);
+  --chart-4: oklch(0.828 0.189 84.429);
+  --chart-5: oklch(0.769 0.188 70.08);
+  --sidebar: oklch(0.985 0 0);
+  --sidebar-foreground: oklch(0.145 0 0);
+  --sidebar-primary: oklch(0.205 0 0);
+  --sidebar-primary-foreground: oklch(0.985 0 0);
+  --sidebar-accent: oklch(0.97 0 0);
+  --sidebar-accent-foreground: oklch(0.205 0 0);
+  --sidebar-border: oklch(0.922 0 0);
+  --sidebar-ring: oklch(0.708 0 0);
 }
 
-body {
-  background: var(--background);
-  color: var(--foreground);
-  font-family: Arial, Helvetica, sans-serif;
+.dark {
+  --background: oklch(0.145 0 0);
+  --foreground: oklch(0.985 0 0);
+  --card: oklch(0.205 0 0);
+  --card-foreground: oklch(0.985 0 0);
+  --popover: oklch(0.205 0 0);
+  --popover-foreground: oklch(0.985 0 0);
+  --primary: oklch(0.922 0 0);
+  --primary-foreground: oklch(0.205 0 0);
+  --secondary: oklch(0.269 0 0);
+  --secondary-foreground: oklch(0.985 0 0);
+  --muted: oklch(0.269 0 0);
+  --muted-foreground: oklch(0.708 0 0);
+  --accent: oklch(0.269 0 0);
+  --accent-foreground: oklch(0.985 0 0);
+  --destructive: oklch(0.704 0.191 22.216);
+  --border: oklch(1 0 0 / 10%);
+  --input: oklch(1 0 0 / 15%);
+  --ring: oklch(0.556 0 0);
+  --chart-1: oklch(0.488 0.243 264.376);
+  --chart-2: oklch(0.696 0.17 162.48);
+  --chart-3: oklch(0.769 0.188 70.08);
+  --chart-4: oklch(0.627 0.265 303.9);
+  --chart-5: oklch(0.645 0.246 16.439);
+  --sidebar: oklch(0.205 0 0);
+  --sidebar-foreground: oklch(0.985 0 0);
+  --sidebar-primary: oklch(0.488 0.243 264.376);
+  --sidebar-primary-foreground: oklch(0.985 0 0);
+  --sidebar-accent: oklch(0.269 0 0);
+  --sidebar-accent-foreground: oklch(0.985 0 0);
+  --sidebar-border: oklch(1 0 0 / 10%);
+  --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+  }
+  body {
+    @apply bg-background text-foreground;
+  }
 }

+ 11 - 13
src/app/layout.tsx

@@ -1,20 +1,16 @@
 import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Inter } from "next/font/google";
 import "./globals.css";
+import { AuthProvider } from "@/components/providers/auth-provider";
 
-const geistSans = Geist({
-  variable: "--font-geist-sans",
-  subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
-  variable: "--font-geist-mono",
+const inter = Inter({
   subsets: ["latin"],
+  variable: "--font-inter",
 });
 
 export const metadata: Metadata = {
-  title: "Create Next App",
-  description: "Generated by create next app",
+  title: "Sistema de Gestión de Asistencia",
+  description: "Sistema de gestión de asistencia para estudiantes universitarios",
 };
 
 export default function RootLayout({
@@ -23,11 +19,13 @@ export default function RootLayout({
   children: React.ReactNode;
 }>) {
   return (
-    <html lang="en">
+    <html lang="es">
       <body
-        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+        className={`${inter.variable} font-sans antialiased`}
       >
-        {children}
+        <AuthProvider>
+          {children}
+        </AuthProvider>
       </body>
     </html>
   );

+ 66 - 97
src/app/page.tsx

@@ -1,103 +1,72 @@
-import Image from "next/image";
+'use client';
+
+import { useSession } from 'next-auth/react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
 
 export default function Home() {
-  return (
-    <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
-      <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
-        <Image
-          className="dark:invert"
-          src="/next.svg"
-          alt="Next.js logo"
-          width={180}
-          height={38}
-          priority
-        />
-        <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
-          <li className="mb-2 tracking-[-.01em]">
-            Get started by editing{" "}
-            <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
-              src/app/page.tsx
-            </code>
-            .
-          </li>
-          <li className="tracking-[-.01em]">
-            Save and see your changes instantly.
-          </li>
-        </ol>
+  const { data: session, status } = useSession();
+  const router = useRouter();
+
+  useEffect(() => {
+    if (status === 'loading') return;
+
+    if (session?.user) {
+      // Redirigir según el rol del usuario
+      switch (session.user.role) {
+        case 'admin':
+          router.push('/admin/dashboard');
+          break;
+        case 'teacher':
+          router.push('/teacher/dashboard');
+          break;
+        case 'student':
+          router.push('/student/dashboard');
+          break;
+        default:
+          router.push('/auth/signin');
+      }
+    }
+  }, [session, status, router]);
 
-        <div className="flex gap-4 items-center flex-col sm:flex-row">
-          <a
-            className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            <Image
-              className="dark:invert"
-              src="/vercel.svg"
-              alt="Vercel logomark"
-              width={20}
-              height={20}
-            />
-            Deploy now
-          </a>
-          <a
-            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
-            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            Read our docs
-          </a>
-        </div>
-      </main>
-      <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/file.svg"
-            alt="File icon"
-            width={16}
-            height={16}
-          />
-          Learn
-        </a>
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/window.svg"
-            alt="Window icon"
-            width={16}
-            height={16}
-          />
-          Examples
-        </a>
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/globe.svg"
-            alt="Globe icon"
-            width={16}
-            height={16}
-          />
-          Go to nextjs.org →
-        </a>
-      </footer>
+  if (status === 'loading') {
+    return (
+      <div className="min-h-screen flex items-center justify-center">
+        <div className="text-lg">Cargando...</div>
+      </div>
+    );
+  }
+
+  if (!session) {
+    return (
+      <div className="min-h-screen flex items-center justify-center bg-gray-50">
+        <Card className="w-full max-w-md">
+          <CardHeader className="text-center">
+            <CardTitle className="text-2xl font-bold">
+              Sistema de Gestión de Asistencia
+            </CardTitle>
+            <CardDescription>
+              Bienvenido al sistema de gestión de asistencia universitaria
+            </CardDescription>
+          </CardHeader>
+          <CardContent className="text-center">
+            <Button 
+              onClick={() => router.push('/auth/signin')}
+              className="w-full"
+            >
+              Iniciar Sesión
+            </Button>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen flex items-center justify-center">
+      <div className="text-lg">Redirigiendo...</div>
     </div>
   );
 }

+ 12 - 0
src/components/providers/auth-provider.tsx

@@ -0,0 +1,12 @@
+'use client';
+
+import { SessionProvider } from 'next-auth/react';
+import { ReactNode } from 'react';
+
+interface AuthProviderProps {
+  children: ReactNode;
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+  return <SessionProvider>{children}</SessionProvider>;
+}

+ 66 - 0
src/components/ui/alert.tsx

@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+  "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+  {
+    variants: {
+      variant: {
+        default: "bg-card text-card-foreground",
+        destructive:
+          "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+    },
+  }
+)
+
+function Alert({
+  className,
+  variant,
+  ...props
+}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
+  return (
+    <div
+      data-slot="alert"
+      role="alert"
+      className={cn(alertVariants({ variant }), className)}
+      {...props}
+    />
+  )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-title"
+      className={cn(
+        "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDescription({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-description"
+      className={cn(
+        "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Alert, AlertTitle, AlertDescription }

+ 59 - 0
src/components/ui/button.tsx

@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+  {
+    variants: {
+      variant: {
+        default:
+          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+        destructive:
+          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        outline:
+          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+        secondary:
+          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+        ghost:
+          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+        link: "text-primary underline-offset-4 hover:underline",
+      },
+      size: {
+        default: "h-9 px-4 py-2 has-[>svg]:px-3",
+        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+        icon: "size-9",
+      },
+    },
+    defaultVariants: {
+      variant: "default",
+      size: "default",
+    },
+  }
+)
+
+function Button({
+  className,
+  variant,
+  size,
+  asChild = false,
+  ...props
+}: React.ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean
+  }) {
+  const Comp = asChild ? Slot : "button"
+
+  return (
+    <Comp
+      data-slot="button"
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+
+export { Button, buttonVariants }

+ 92 - 0
src/components/ui/card.tsx

@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card"
+      className={cn(
+        "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-header"
+      className={cn(
+        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-title"
+      className={cn("leading-none font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-action"
+      className={cn(
+        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-content"
+      className={cn("px-6", className)}
+      {...props}
+    />
+  )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="card-footer"
+      className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  Card,
+  CardHeader,
+  CardFooter,
+  CardTitle,
+  CardAction,
+  CardDescription,
+  CardContent,
+}

+ 21 - 0
src/components/ui/input.tsx

@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+  return (
+    <input
+      type={type}
+      data-slot="input"
+      className={cn(
+        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Input }

+ 24 - 0
src/components/ui/label.tsx

@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+  className,
+  ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+  return (
+    <LabelPrimitive.Root
+      data-slot="label"
+      className={cn(
+        "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+export { Label }

+ 185 - 0
src/components/ui/select.tsx

@@ -0,0 +1,185 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Root>) {
+  return <SelectPrimitive.Root data-slot="select" {...props} />
+}
+
+function SelectGroup({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Group>) {
+  return <SelectPrimitive.Group data-slot="select-group" {...props} />
+}
+
+function SelectValue({
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Value>) {
+  return <SelectPrimitive.Value data-slot="select-value" {...props} />
+}
+
+function SelectTrigger({
+  className,
+  size = "default",
+  children,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
+  size?: "sm" | "default"
+}) {
+  return (
+    <SelectPrimitive.Trigger
+      data-slot="select-trigger"
+      data-size={size}
+      className={cn(
+        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+        className
+      )}
+      {...props}
+    >
+      {children}
+      <SelectPrimitive.Icon asChild>
+        <ChevronDownIcon className="size-4 opacity-50" />
+      </SelectPrimitive.Icon>
+    </SelectPrimitive.Trigger>
+  )
+}
+
+function SelectContent({
+  className,
+  children,
+  position = "popper",
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+  return (
+    <SelectPrimitive.Portal>
+      <SelectPrimitive.Content
+        data-slot="select-content"
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
+          position === "popper" &&
+            "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+          className
+        )}
+        position={position}
+        {...props}
+      >
+        <SelectScrollUpButton />
+        <SelectPrimitive.Viewport
+          className={cn(
+            "p-1",
+            position === "popper" &&
+              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
+          )}
+        >
+          {children}
+        </SelectPrimitive.Viewport>
+        <SelectScrollDownButton />
+      </SelectPrimitive.Content>
+    </SelectPrimitive.Portal>
+  )
+}
+
+function SelectLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Label>) {
+  return (
+    <SelectPrimitive.Label
+      data-slot="select-label"
+      className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
+      {...props}
+    />
+  )
+}
+
+function SelectItem({
+  className,
+  children,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Item>) {
+  return (
+    <SelectPrimitive.Item
+      data-slot="select-item"
+      className={cn(
+        "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+        className
+      )}
+      {...props}
+    >
+      <span className="absolute right-2 flex size-3.5 items-center justify-center">
+        <SelectPrimitive.ItemIndicator>
+          <CheckIcon className="size-4" />
+        </SelectPrimitive.ItemIndicator>
+      </span>
+      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+    </SelectPrimitive.Item>
+  )
+}
+
+function SelectSeparator({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
+  return (
+    <SelectPrimitive.Separator
+      data-slot="select-separator"
+      className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+      {...props}
+    />
+  )
+}
+
+function SelectScrollUpButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
+  return (
+    <SelectPrimitive.ScrollUpButton
+      data-slot="select-scroll-up-button"
+      className={cn(
+        "flex cursor-default items-center justify-center py-1",
+        className
+      )}
+      {...props}
+    >
+      <ChevronUpIcon className="size-4" />
+    </SelectPrimitive.ScrollUpButton>
+  )
+}
+
+function SelectScrollDownButton({
+  className,
+  ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
+  return (
+    <SelectPrimitive.ScrollDownButton
+      data-slot="select-scroll-down-button"
+      className={cn(
+        "flex cursor-default items-center justify-center py-1",
+        className
+      )}
+      {...props}
+    >
+      <ChevronDownIcon className="size-4" />
+    </SelectPrimitive.ScrollDownButton>
+  )
+}
+
+export {
+  Select,
+  SelectContent,
+  SelectGroup,
+  SelectItem,
+  SelectLabel,
+  SelectScrollDownButton,
+  SelectScrollUpButton,
+  SelectSeparator,
+  SelectTrigger,
+  SelectValue,
+}

+ 78 - 0
src/lib/auth.ts

@@ -0,0 +1,78 @@
+import { NextAuthOptions } from 'next-auth';
+import CredentialsProvider from 'next-auth/providers/credentials';
+import { DrizzleAdapter } from '@auth/drizzle-adapter';
+import { db } from './db';
+import { users } from './db/schema';
+import { eq } from 'drizzle-orm';
+import bcrypt from 'bcryptjs';
+import type { Adapter } from 'next-auth/adapters';
+
+export const authOptions: NextAuthOptions = {
+  adapter: DrizzleAdapter(db) as Adapter,
+  providers: [
+    CredentialsProvider({
+      name: 'credentials',
+      credentials: {
+        email: { label: 'Email', type: 'email' },
+        password: { label: 'Password', type: 'password' },
+      },
+      async authorize(credentials: Record<string, string> | undefined) {
+        if (!credentials?.email || !credentials?.password) {
+          return null;
+        }
+
+        try {
+          const user = await db
+            .select()
+            .from(users)
+            .where(eq(users.email, credentials.email))
+            .limit(1);
+
+          if (!user[0] || !user[0].isActive) {
+            return null;
+          }
+
+          const isPasswordValid = await bcrypt.compare(
+            credentials.password,
+            user[0].password
+          );
+
+          if (!isPasswordValid) {
+            return null;
+          }
+
+          return {
+            id: user[0].id,
+            email: user[0].email,
+            name: `${user[0].firstName} ${user[0].lastName}`,
+            role: user[0].role,
+          };
+        } catch (error) {
+          console.error('Auth error:', error);
+          return null;
+        }
+      },
+    }),
+  ],
+  session: {
+    strategy: 'jwt',
+  },
+  callbacks: {
+    async jwt({ token, user }) {
+      if (user) {
+        token.role = user.role;
+      }
+      return token;
+    },
+    async session({ session, token }) {
+      if (token) {
+        session.user.id = token.sub!;
+        session.user.role = token.role as string;
+      }
+      return session;
+    },
+  },
+  pages: {
+    signIn: '/auth/signin',
+  },
+};

+ 10 - 0
src/lib/db/index.ts

@@ -0,0 +1,10 @@
+import { drizzle } from 'drizzle-orm/postgres-js';
+import postgres from 'postgres';
+import * as schema from './schema';
+
+if (!process.env.DATABASE_URL) {
+  throw new Error('DATABASE_URL is not defined');
+}
+
+const client = postgres(process.env.DATABASE_URL);
+export const db = drizzle(client, { schema });

+ 199 - 0
src/lib/db/schema.ts

@@ -0,0 +1,199 @@
+import { pgTable, text, timestamp, integer, boolean, uuid, varchar, date } from 'drizzle-orm/pg-core';
+import { relations } from 'drizzle-orm';
+import { eq, and, ne } from 'drizzle-orm';
+
+// Re-export drizzle operators for convenience
+export { eq, and, ne };
+
+// Tabla de usuarios (administradores, docentes, estudiantes)
+export const users = pgTable('users', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  email: varchar('email', { length: 255 }).notNull().unique(),
+  password: text('password').notNull(),
+  firstName: varchar('first_name', { length: 100 }).notNull(),
+  lastName: varchar('last_name', { length: 100 }).notNull(),
+  cedula: varchar('cedula', { length: 20 }).notNull().unique(),
+  phone: varchar('phone', { length: 20 }),
+  role: varchar('role', { length: 20 }).notNull(), // 'admin', 'teacher', 'student'
+  admissionNumber: varchar('admission_number', { length: 50 }), // Solo para estudiantes
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+  updatedAt: timestamp('updated_at').defaultNow(),
+});
+
+// Tabla de periodos
+export const periods = pgTable('periods', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  name: varchar('name', { length: 100 }).notNull(),
+  startDate: date('start_date').notNull(),
+  endDate: date('end_date').notNull(),
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+  updatedAt: timestamp('updated_at').defaultNow(),
+});
+
+// Tabla de parciales
+export const partials = pgTable('partials', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  name: varchar('name', { length: 100 }).notNull(),
+  periodId: uuid('period_id').references(() => periods.id),
+  startDate: date('start_date').notNull(),
+  endDate: date('end_date').notNull(),
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+  updatedAt: timestamp('updated_at').defaultNow(),
+});
+
+// Tabla de clases/materias
+export const classes = pgTable('classes', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  name: varchar('name', { length: 100 }).notNull(),
+  code: varchar('code', { length: 20 }).notNull().unique(),
+  credits: integer('credits').notNull(),
+  description: text('description'),
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+  updatedAt: timestamp('updated_at').defaultNow(),
+});
+
+// Tabla de paralelos/secciones
+export const sections = pgTable('sections', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  name: varchar('name', { length: 50 }).notNull(),
+  classId: uuid('class_id').references(() => classes.id),
+  periodId: uuid('period_id').references(() => periods.id),
+  maxStudents: integer('max_students').notNull(),
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+  updatedAt: timestamp('updated_at').defaultNow(),
+});
+
+// Tabla de asignaciones de docentes a clases/secciones
+export const teacherAssignments = pgTable('teacher_assignments', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  teacherId: uuid('teacher_id').references(() => users.id),
+  classId: uuid('class_id').references(() => classes.id),
+  sectionId: uuid('section_id').references(() => sections.id),
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+});
+
+// Tabla de inscripciones de estudiantes
+export const studentEnrollments = pgTable('student_enrollments', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  studentId: uuid('student_id').references(() => users.id),
+  classId: uuid('class_id').references(() => classes.id),
+  sectionId: uuid('section_id').references(() => sections.id),
+  isActive: boolean('is_active').default(true),
+  createdAt: timestamp('created_at').defaultNow(),
+});
+
+// Tabla de asistencia
+export const attendance = pgTable('attendance', {
+  id: uuid('id').defaultRandom().primaryKey(),
+  studentId: uuid('student_id').references(() => users.id),
+  classId: uuid('class_id').references(() => classes.id),
+  sectionId: uuid('section_id').references(() => sections.id),
+  teacherId: uuid('teacher_id').references(() => users.id),
+  partialId: uuid('partial_id').references(() => partials.id),
+  date: date('date').notNull(),
+  status: varchar('status', { length: 20 }).notNull(), // 'present', 'absent', 'justified'
+  reason: text('reason'), // Motivo de falta o justificación
+  createdAt: timestamp('created_at').defaultNow(),
+});
+
+// Relaciones
+export const usersRelations = relations(users, ({ many }) => ({
+  teacherAssignments: many(teacherAssignments),
+  studentEnrollments: many(studentEnrollments),
+  attendanceAsStudent: many(attendance, { relationName: 'studentAttendance' }),
+  attendanceAsTeacher: many(attendance, { relationName: 'teacherAttendance' }),
+}));
+
+export const periodsRelations = relations(periods, ({ many }) => ({
+  partials: many(partials),
+  classes: many(classes),
+}));
+
+export const partialsRelations = relations(partials, ({ one }) => ({
+  period: one(periods, {
+    fields: [partials.periodId],
+    references: [periods.id],
+  }),
+}));
+
+export const classesRelations = relations(classes, ({ many }) => ({
+  sections: many(sections),
+  teacherAssignments: many(teacherAssignments),
+  studentEnrollments: many(studentEnrollments),
+  attendance: many(attendance),
+}));
+
+export const sectionsRelations = relations(sections, ({ one, many }) => ({
+  class: one(classes, {
+    fields: [sections.classId],
+    references: [classes.id],
+  }),
+  period: one(periods, {
+    fields: [sections.periodId],
+    references: [periods.id],
+  }),
+  teacherAssignments: many(teacherAssignments),
+  studentEnrollments: many(studentEnrollments),
+  attendance: many(attendance),
+}));
+
+export const teacherAssignmentsRelations = relations(teacherAssignments, ({ one }) => ({
+  teacher: one(users, {
+    fields: [teacherAssignments.teacherId],
+    references: [users.id],
+  }),
+  class: one(classes, {
+    fields: [teacherAssignments.classId],
+    references: [classes.id],
+  }),
+  section: one(sections, {
+    fields: [teacherAssignments.sectionId],
+    references: [sections.id],
+  }),
+}));
+
+export const studentEnrollmentsRelations = relations(studentEnrollments, ({ one }) => ({
+  student: one(users, {
+    fields: [studentEnrollments.studentId],
+    references: [users.id],
+  }),
+  class: one(classes, {
+    fields: [studentEnrollments.classId],
+    references: [classes.id],
+  }),
+  section: one(sections, {
+    fields: [studentEnrollments.sectionId],
+    references: [sections.id],
+  }),
+}));
+
+export const attendanceRelations = relations(attendance, ({ one }) => ({
+  student: one(users, {
+    fields: [attendance.studentId],
+    references: [users.id],
+    relationName: 'studentAttendance',
+  }),
+  teacher: one(users, {
+    fields: [attendance.teacherId],
+    references: [users.id],
+    relationName: 'teacherAttendance',
+  }),
+  class: one(classes, {
+    fields: [attendance.classId],
+    references: [classes.id],
+  }),
+  section: one(sections, {
+    fields: [attendance.sectionId],
+    references: [sections.id],
+  }),
+  partial: one(partials, {
+    fields: [attendance.partialId],
+    references: [partials.id],
+  }),
+}));

+ 157 - 0
src/lib/db/seed.ts

@@ -0,0 +1,157 @@
+import { db } from './index';
+import { users, periods, partials, classes, sections, teacherAssignments, studentEnrollments } from './schema';
+import bcrypt from 'bcryptjs';
+
+export async function seedDatabase() {
+  try {
+    console.log('🌱 Iniciando seed de la base de datos...');
+
+    // Crear usuario administrador
+    const hashedPassword = await bcrypt.hash('admin123', 10);
+    
+    const [admin] = await db.insert(users).values({
+      email: 'admin@universidad.edu',
+      password: hashedPassword,
+      firstName: 'Administrador',
+      lastName: 'Sistema',
+      cedula: '1234567890',
+      phone: '0999999999',
+      role: 'admin',
+    }).returning();
+
+    // Crear periodo académico
+    const [period] = await db.insert(periods).values({
+      name: '2024-2025',
+      startDate: '2024-09-01',
+      endDate: '2025-06-30',
+    }).returning();
+
+    // Crear parciales
+    const [partial1] = await db.insert(partials).values({
+      name: 'Primer Parcial',
+      periodId: period.id,
+      startDate: '2024-09-01',
+      endDate: '2024-11-15',
+    }).returning();
+
+    const [partial2] = await db.insert(partials).values({
+      name: 'Segundo Parcial',
+      periodId: period.id,
+      startDate: '2024-11-16',
+      endDate: '2025-02-15',
+    }).returning();
+
+    // Crear clases
+    const [class1] = await db.insert(classes).values({
+      name: 'Programación I',
+      code: 'PROG001',
+      credits: 4,
+      description: 'Introducción a la programación con fundamentos básicos',
+    }).returning();
+
+    const [class2] = await db.insert(classes).values({
+      name: 'Matemáticas I',
+      code: 'MATH001',
+      credits: 3,
+      description: 'Fundamentos matemáticos para ingeniería',
+    }).returning();
+
+    // Crear secciones
+    const [section1A] = await db.insert(sections).values({
+      name: 'A',
+      classId: class1.id,
+      periodId: period.id,
+      maxStudents: 30,
+    }).returning();
+
+    const [section1B] = await db.insert(sections).values({
+      name: 'B',
+      classId: class1.id,
+      periodId: period.id,
+      maxStudents: 25,
+    }).returning();
+
+    const [section2A] = await db.insert(sections).values({
+      name: 'A',
+      classId: class2.id,
+      periodId: period.id,
+      maxStudents: 35,
+    }).returning();
+
+    // Crear profesor
+    const teacherPassword = await bcrypt.hash('teacher123', 10);
+    const [teacher] = await db.insert(users).values({
+      email: 'profesor@universidad.edu',
+      password: teacherPassword,
+      firstName: 'Juan',
+      lastName: 'Pérez',
+      cedula: '0987654321',
+      phone: '0988888888',
+      role: 'teacher',
+    }).returning();
+
+    // Asignar profesor a clases
+    await db.insert(teacherAssignments).values([
+      {
+        teacherId: teacher.id,
+        classId: class1.id,
+        sectionId: section1A.id,
+      },
+      {
+        teacherId: teacher.id,
+        classId: class1.id,
+        sectionId: section1B.id,
+      },
+    ]);
+
+    // Crear estudiantes
+    const studentPassword = await bcrypt.hash('student123', 10);
+    
+    const students = [];
+    for (let i = 1; i <= 5; i++) {
+      const [student] = await db.insert(users).values({
+        email: `estudiante${i}@universidad.edu`,
+        password: studentPassword,
+        firstName: `Estudiante`,
+        lastName: `${i}`,
+        cedula: `123456789${i}`,
+        phone: `099888888${i}`,
+        role: 'student',
+        admissionNumber: `2024-${String(i).padStart(4, '0')}`,
+      }).returning();
+      students.push(student);
+    }
+
+    // Inscribir estudiantes en clases
+    for (const student of students.slice(0, 3)) {
+      await db.insert(studentEnrollments).values({
+        studentId: student.id,
+        classId: class1.id,
+        sectionId: section1A.id,
+      });
+    }
+
+    for (const student of students.slice(3, 5)) {
+      await db.insert(studentEnrollments).values({
+        studentId: student.id,
+        classId: class1.id,
+        sectionId: section1B.id,
+      });
+    }
+
+    console.log('✅ Seed completado exitosamente!');
+    console.log('👤 Admin: admin@universidad.edu / admin123');
+    console.log('👨‍🏫 Profesor: profesor@universidad.edu / teacher123');
+    console.log('👨‍🎓 Estudiantes: estudiante1@universidad.edu / student123 (hasta estudiante5)');
+    
+  } catch (error) {
+    console.error('❌ Error durante el seed:', error);
+    throw error;
+  }
+}
+
+if (require.main === module) {
+  seedDatabase()
+    .then(() => process.exit(0))
+    .catch(() => process.exit(1));
+}

+ 6 - 0
src/lib/utils.ts

@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs))
+}

+ 48 - 0
src/middleware.ts

@@ -0,0 +1,48 @@
+import { withAuth } from 'next-auth/middleware';
+import { NextResponse } from 'next/server';
+
+export default withAuth(
+  function middleware(req) {
+    const token = req.nextauth.token;
+    const { pathname } = req.nextUrl;
+
+    // Rutas públicas
+    if (pathname.startsWith('/auth')) {
+      return NextResponse.next();
+    }
+
+    // Verificar si el usuario está autenticado
+    if (!token) {
+      return NextResponse.redirect(new URL('/auth/signin', req.url));
+    }
+
+    // Verificar permisos por rol
+    if (pathname.startsWith('/admin') && token.role !== 'admin') {
+      return NextResponse.redirect(new URL('/unauthorized', req.url));
+    }
+
+    if (pathname.startsWith('/teacher') && token.role !== 'teacher') {
+      return NextResponse.redirect(new URL('/unauthorized', req.url));
+    }
+
+    if (pathname.startsWith('/student') && token.role !== 'student') {
+      return NextResponse.redirect(new URL('/unauthorized', req.url));
+    }
+
+    return NextResponse.next();
+  },
+  {
+    callbacks: {
+      authorized: ({ token }) => !!token,
+    },
+  }
+);
+
+export const config = {
+  matcher: [
+    '/admin/:path*',
+    '/teacher/:path*',
+    '/student/:path*',
+    '/dashboard/:path*',
+  ],
+};

+ 25 - 0
src/types/next-auth.d.ts

@@ -0,0 +1,25 @@
+import NextAuth from 'next-auth';
+
+declare module 'next-auth' {
+  interface Session {
+    user: {
+      id: string;
+      email: string;
+      name: string;
+      role: string;
+    };
+  }
+
+  interface User {
+    id: string;
+    email: string;
+    name: string;
+    role: string;
+  }
+}
+
+declare module 'next-auth/jwt' {
+  interface JWT {
+    role: string;
+  }
+}

Some files were not shown because too many files changed in this diff