From 49c74ab868c325553da50cd53489dba3b8e2b566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Eduardo=20Garc=C3=ADa=20M=C3=A1rquez?= Date: Thu, 8 Jan 2026 09:37:19 -0500 Subject: [PATCH] feat(frontend): implement student self-registration and dashboard - Make name and email required in registration form - Add StudentDashboard component with enrolled subjects and program info - Implement role-based navigation (admin sees management, student sees portal) - Add adminGuard for restricting student management routes - Redirect to /dashboard after login/register instead of /students - Add navigation links: Mi Portal, Mis Materias, Companeros Fulfills test requirements: - Students can register online (punto 1) - Enroll in up to 3 subjects/9 credits (puntos 2-5) - Cannot have same professor twice (punto 7) - View classmates by subject - names only (puntos 8-9) --- src/frontend/src/app/app.component.ts | 20 +- src/frontend/src/app/app.routes.ts | 17 +- .../src/app/core/guards/auth.guard.ts | 8 +- .../auth/pages/login/login.component.ts | 2 +- .../auth/pages/register/register.component.ts | 34 +- .../student-dashboard.component.ts | 488 ++++++++++++++++++ 6 files changed, 546 insertions(+), 23 deletions(-) create mode 100644 src/frontend/src/app/features/dashboard/pages/student-dashboard/student-dashboard.component.ts diff --git a/src/frontend/src/app/app.component.ts b/src/frontend/src/app/app.component.ts index c4ee4ba..f3e8766 100644 --- a/src/frontend/src/app/app.component.ts +++ b/src/frontend/src/app/app.component.ts @@ -28,9 +28,23 @@ import { ConnectivityOverlayComponent } from '@shared/index'; Estudiantes
- + + @if (showError('name')) { + + @if (form.get('name')?.errors?.['required']) { + El nombre es requerido + } @else if (form.get('name')?.errors?.['minlength']) { + Minimo 3 caracteres + } + + }
- + @if (showError('email')) { - Email invalido + + @if (form.get('email')?.errors?.['required']) { + El email es requerido + } @else { + Email invalido + } + }
-

Si proporcionas nombre y email, se creara tu perfil de estudiante automaticamente.

- @if (serverError()) {
error @@ -265,8 +279,8 @@ export class RegisterComponent { form = this.fb.group({ username: ['', [Validators.required, Validators.minLength(3)]], password: ['', [Validators.required, Validators.minLength(6)]], - name: [''], - email: ['', Validators.email], + name: ['', [Validators.required, Validators.minLength(3)]], + email: ['', [Validators.required, Validators.email]], }); loading = signal(false); @@ -291,14 +305,14 @@ export class RegisterComponent { this.authService.register( username!, password!, - name || undefined, - email || undefined + name!, + email! ).subscribe({ next: (response) => { this.loading.set(false); if (response.success) { this.notification.success('Cuenta creada exitosamente!'); - this.router.navigate(['/students']); + this.router.navigate(['/dashboard']); } else { this.serverError.set(response.error ?? 'Error al crear la cuenta'); } diff --git a/src/frontend/src/app/features/dashboard/pages/student-dashboard/student-dashboard.component.ts b/src/frontend/src/app/features/dashboard/pages/student-dashboard/student-dashboard.component.ts new file mode 100644 index 0000000..507d85d --- /dev/null +++ b/src/frontend/src/app/features/dashboard/pages/student-dashboard/student-dashboard.component.ts @@ -0,0 +1,488 @@ +import { Component, inject, signal, computed, OnInit } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { Apollo } from 'apollo-angular'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { AuthService } from '@core/services/auth.service'; +import { GET_STUDENT } from '@core/graphql/queries/students.queries'; + +interface Enrollment { + id: number; + subjectId: number; + subjectName: string; + credits: number; + professorName: string; + enrolledAt: string; +} + +interface Student { + id: number; + name: string; + email: string; + totalCredits: number; + enrollments: Enrollment[]; +} + +@Component({ + selector: 'app-student-dashboard', + standalone: true, + imports: [RouterLink, MatIconModule, MatButtonModule, MatProgressSpinnerModule], + template: ` +
+ @if (loading()) { +
+ +

Cargando tu informacion...

+
+ } @else if (error()) { +
+ error_outline +

Error al cargar datos

+

{{ error() }}

+ +
+ } @else if (student()) { +
+
+

Bienvenido, {{ student()!.name }}

+ +
+
+ {{ student()!.totalCredits }} + Creditos +
+
+ +
+ +
+
+

+ school + Mis Materias +

+ {{ student()!.enrollments.length }}/3 +
+ + @if (student()!.enrollments.length === 0) { +
+ menu_book +

Aun no estas inscrito en ninguna materia

+ + add + Inscribirme ahora + +
+ } @else { +
    + @for (enrollment of student()!.enrollments; track enrollment.id) { +
  • +
    + {{ enrollment.subjectName }} + Prof. {{ enrollment.professorName }} +
    + {{ enrollment.credits }} creditos +
  • + } +
+ + @if (canEnrollMore()) { + + } + } +
+ + +
+
+

+ flash_on + Acciones Rapidas +

+
+ + +
+ + +
+
+

+ info + Programa de Creditos +

+
+ +
+
+ book +
+ 10 materias + disponibles en total +
+
+
+ stars +
+ 3 creditos + por cada materia +
+
+
+ check_circle +
+ Maximo 3 materias + puedes inscribir (9 creditos) +
+
+
+ person +
+ 5 profesores + cada uno imparte 2 materias +
+
+
+ warning +
+ Restriccion + No puedes tener 2 materias con el mismo profesor +
+
+
+
+
+ } +
+ `, + styles: [` + .dashboard { + max-width: 1000px; + margin: 0 auto; + } + + .loading-container, .error-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + gap: 1rem; + text-align: center; + } + + .error-container mat-icon { + font-size: 64px; + width: 64px; + height: 64px; + color: var(--error); + } + + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding: 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 1rem; + color: white; + } + + .welcome h1 { + margin: 0 0 0.25rem; + font-size: 1.5rem; + font-weight: 600; + } + + .welcome .email { + margin: 0; + opacity: 0.9; + font-size: 0.875rem; + } + + .credits-badge { + display: flex; + flex-direction: column; + align-items: center; + background: rgba(255, 255, 255, 0.2); + padding: 1rem 1.5rem; + border-radius: 0.75rem; + } + + .credits-value { + font-size: 2rem; + font-weight: 700; + } + + .credits-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.9; + } + + .dashboard-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: 1fr; + } + + @media (min-width: 768px) { + .dashboard-grid { + grid-template-columns: 1fr 1fr; + } + + .enrollments-card { + grid-row: span 2; + } + } + + .card { + background: white; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } + + .card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + } + + .card-header h2 { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + font-size: 1.125rem; + font-weight: 600; + } + + .card-header mat-icon { + color: var(--accent); + } + + .count-badge { + background: var(--bg-secondary); + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.875rem; + font-weight: 500; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + text-align: center; + color: var(--text-secondary); + } + + .empty-state mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + margin-bottom: 1rem; + opacity: 0.5; + } + + .empty-state p { + margin: 0 0 1rem; + } + + .enrollments-list { + list-style: none; + padding: 0; + margin: 0; + } + + .enrollment-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-radius: 0.5rem; + background: var(--bg-secondary); + margin-bottom: 0.5rem; + } + + .subject-info { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .subject-name { + font-weight: 500; + } + + .professor { + font-size: 0.8125rem; + color: var(--text-secondary); + } + + .credits { + font-size: 0.8125rem; + color: var(--text-secondary); + background: white; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + } + + .card-actions { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-light); + } + + .actions-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .action-btn { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: var(--bg-secondary); + border-radius: 0.75rem; + text-decoration: none; + color: inherit; + transition: all 0.2s; + } + + .action-btn:hover { + background: var(--border-light); + transform: translateX(4px); + } + + .action-btn mat-icon { + color: var(--accent); + } + + .action-btn span { + font-weight: 500; + } + + .action-btn small { + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + font-weight: normal; + } + + .info-content { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .info-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 0.5rem; + } + + .info-item mat-icon { + color: var(--accent); + } + + .info-item div { + display: flex; + flex-direction: column; + } + + .info-item strong { + font-size: 0.875rem; + } + + .info-item span { + font-size: 0.75rem; + color: var(--text-secondary); + } + + .info-item.warning { + background: rgba(245, 158, 11, 0.1); + } + + .info-item.warning mat-icon { + color: #f59e0b; + } + `] +}) +export class StudentDashboardComponent implements OnInit { + private apollo = inject(Apollo); + private authService = inject(AuthService); + + student = signal(null); + loading = signal(true); + error = signal(null); + + studentId = computed(() => this.authService.studentId()); + canEnrollMore = computed(() => (this.student()?.enrollments.length ?? 0) < 3); + + ngOnInit(): void { + this.loadStudentData(); + } + + loadStudentData(): void { + const id = this.studentId(); + if (!id) { + this.error.set('No se encontro tu perfil de estudiante'); + this.loading.set(false); + return; + } + + this.loading.set(true); + this.error.set(null); + + this.apollo + .query<{ student: Student }>({ + query: GET_STUDENT, + variables: { id }, + fetchPolicy: 'network-only', + }) + .subscribe({ + next: (result) => { + if (result.data?.student) { + this.student.set(result.data.student); + } + this.loading.set(false); + }, + error: (err) => { + this.error.set(err.message || 'Error al cargar datos'); + this.loading.set(false); + }, + }); + } +}