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)
This commit is contained in:
parent
9d8c6f0331
commit
49c74ab868
|
|
@ -28,9 +28,23 @@ import { ConnectivityOverlayComponent } from '@shared/index';
|
||||||
<span class="logo-text">Estudiantes</span>
|
<span class="logo-text">Estudiantes</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
<a routerLink="/students" routerLinkActive="active" class="nav-link">
|
@if (authService.isAdmin()) {
|
||||||
Estudiantes
|
<a routerLink="/students" routerLinkActive="active" class="nav-link">
|
||||||
</a>
|
Gestion Estudiantes
|
||||||
|
</a>
|
||||||
|
} @else {
|
||||||
|
<a routerLink="/dashboard" routerLinkActive="active" class="nav-link">
|
||||||
|
Mi Portal
|
||||||
|
</a>
|
||||||
|
@if (authService.studentId()) {
|
||||||
|
<a [routerLink]="['/enrollment', authService.studentId()]" routerLinkActive="active" class="nav-link">
|
||||||
|
Mis Materias
|
||||||
|
</a>
|
||||||
|
<a [routerLink]="['/classmates', authService.studentId()]" routerLinkActive="active" class="nav-link">
|
||||||
|
Companeros
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="user-menu">
|
<div class="user-menu">
|
||||||
<button class="user-button" [matMenuTriggerFor]="userMenu">
|
<button class="user-button" [matMenuTriggerFor]="userMenu">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { authGuard, guestGuard } from '@core/guards/auth.guard';
|
import { authGuard, guestGuard, adminGuard } from '@core/guards/auth.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@features/dashboard/pages/student-dashboard/student-dashboard.component')
|
||||||
|
.then(m => m.StudentDashboardComponent),
|
||||||
|
canActivate: [authGuard],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
|
|
@ -22,21 +29,21 @@ export const routes: Routes = [
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('@features/students/pages/student-list/student-list.component')
|
import('@features/students/pages/student-list/student-list.component')
|
||||||
.then(m => m.StudentListComponent),
|
.then(m => m.StudentListComponent),
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard, adminGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'students/new',
|
path: 'students/new',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('@features/students/pages/student-form/student-form.component')
|
import('@features/students/pages/student-form/student-form.component')
|
||||||
.then(m => m.StudentFormComponent),
|
.then(m => m.StudentFormComponent),
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard, adminGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'students/:id/edit',
|
path: 'students/:id/edit',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('@features/students/pages/student-form/student-form.component')
|
import('@features/students/pages/student-form/student-form.component')
|
||||||
.then(m => m.StudentFormComponent),
|
.then(m => m.StudentFormComponent),
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard, adminGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'enrollment/:studentId',
|
path: 'enrollment/:studentId',
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const authGuard: CanActivateFn = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guard that requires admin role to access a route.
|
* Guard that requires admin role to access a route.
|
||||||
* Redirects to home if not admin.
|
* Redirects to dashboard if not admin.
|
||||||
*/
|
*/
|
||||||
export const adminGuard: CanActivateFn = () => {
|
export const adminGuard: CanActivateFn = () => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
|
@ -30,13 +30,13 @@ export const adminGuard: CanActivateFn = () => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.navigate(['/students']);
|
router.navigate(['/dashboard']);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guard that prevents authenticated users from accessing a route.
|
* Guard that prevents authenticated users from accessing a route.
|
||||||
* Useful for login page - redirects to home if already logged in.
|
* Useful for login page - redirects to dashboard if already logged in.
|
||||||
*/
|
*/
|
||||||
export const guestGuard: CanActivateFn = () => {
|
export const guestGuard: CanActivateFn = () => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
|
@ -46,6 +46,6 @@ export const guestGuard: CanActivateFn = () => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.navigate(['/students']);
|
router.navigate(['/dashboard']);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ export class LoginComponent {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.notification.success('Bienvenido!');
|
this.notification.success('Bienvenido!');
|
||||||
this.router.navigate(['/students']);
|
this.router.navigate(['/dashboard']);
|
||||||
} else {
|
} else {
|
||||||
this.serverError.set(response.error ?? 'Error al iniciar sesion');
|
this.serverError.set(response.error ?? 'Error al iniciar sesion');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,18 +63,28 @@ import { NotificationService } from '@core/services';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Nombre completo (opcional)</label>
|
<label for="name">Nombre completo</label>
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
class="input"
|
class="input"
|
||||||
formControlName="name"
|
formControlName="name"
|
||||||
placeholder="Tu nombre completo"
|
placeholder="Tu nombre completo"
|
||||||
|
[class.error]="showError('name')"
|
||||||
/>
|
/>
|
||||||
|
@if (showError('name')) {
|
||||||
|
<span class="error-message">
|
||||||
|
@if (form.get('name')?.errors?.['required']) {
|
||||||
|
El nombre es requerido
|
||||||
|
} @else if (form.get('name')?.errors?.['minlength']) {
|
||||||
|
Minimo 3 caracteres
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email (opcional)</label>
|
<label for="email">Email</label>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -84,12 +94,16 @@ import { NotificationService } from '@core/services';
|
||||||
[class.error]="showError('email')"
|
[class.error]="showError('email')"
|
||||||
/>
|
/>
|
||||||
@if (showError('email')) {
|
@if (showError('email')) {
|
||||||
<span class="error-message">Email invalido</span>
|
<span class="error-message">
|
||||||
|
@if (form.get('email')?.errors?.['required']) {
|
||||||
|
El email es requerido
|
||||||
|
} @else {
|
||||||
|
Email invalido
|
||||||
|
}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="hint">Si proporcionas nombre y email, se creara tu perfil de estudiante automaticamente.</p>
|
|
||||||
|
|
||||||
@if (serverError()) {
|
@if (serverError()) {
|
||||||
<div class="server-error">
|
<div class="server-error">
|
||||||
<mat-icon>error</mat-icon>
|
<mat-icon>error</mat-icon>
|
||||||
|
|
@ -265,8 +279,8 @@ export class RegisterComponent {
|
||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
username: ['', [Validators.required, Validators.minLength(3)]],
|
username: ['', [Validators.required, Validators.minLength(3)]],
|
||||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||||
name: [''],
|
name: ['', [Validators.required, Validators.minLength(3)]],
|
||||||
email: ['', Validators.email],
|
email: ['', [Validators.required, Validators.email]],
|
||||||
});
|
});
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|
@ -291,14 +305,14 @@ export class RegisterComponent {
|
||||||
this.authService.register(
|
this.authService.register(
|
||||||
username!,
|
username!,
|
||||||
password!,
|
password!,
|
||||||
name || undefined,
|
name!,
|
||||||
email || undefined
|
email!
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.notification.success('Cuenta creada exitosamente!');
|
this.notification.success('Cuenta creada exitosamente!');
|
||||||
this.router.navigate(['/students']);
|
this.router.navigate(['/dashboard']);
|
||||||
} else {
|
} else {
|
||||||
this.serverError.set(response.error ?? 'Error al crear la cuenta');
|
this.serverError.set(response.error ?? 'Error al crear la cuenta');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: `
|
||||||
|
<div class="dashboard">
|
||||||
|
@if (loading()) {
|
||||||
|
<div class="loading-container">
|
||||||
|
<mat-spinner diameter="48"></mat-spinner>
|
||||||
|
<p>Cargando tu informacion...</p>
|
||||||
|
</div>
|
||||||
|
} @else if (error()) {
|
||||||
|
<div class="error-container">
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
|
<h2>Error al cargar datos</h2>
|
||||||
|
<p>{{ error() }}</p>
|
||||||
|
<button class="btn btn-primary" (click)="loadStudentData()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else if (student()) {
|
||||||
|
<header class="dashboard-header">
|
||||||
|
<div class="welcome">
|
||||||
|
<h1>Bienvenido, {{ student()!.name }}</h1>
|
||||||
|
<p class="email">{{ student()!.email }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="credits-badge">
|
||||||
|
<span class="credits-value">{{ student()!.totalCredits }}</span>
|
||||||
|
<span class="credits-label">Creditos</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<!-- Enrolled Subjects Card -->
|
||||||
|
<section class="card enrollments-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>
|
||||||
|
<mat-icon>school</mat-icon>
|
||||||
|
Mis Materias
|
||||||
|
</h2>
|
||||||
|
<span class="count-badge">{{ student()!.enrollments.length }}/3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (student()!.enrollments.length === 0) {
|
||||||
|
<div class="empty-state">
|
||||||
|
<mat-icon>menu_book</mat-icon>
|
||||||
|
<p>Aun no estas inscrito en ninguna materia</p>
|
||||||
|
<a [routerLink]="['/enrollment', studentId()]" class="btn btn-primary">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
Inscribirme ahora
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<ul class="enrollments-list">
|
||||||
|
@for (enrollment of student()!.enrollments; track enrollment.id) {
|
||||||
|
<li class="enrollment-item">
|
||||||
|
<div class="subject-info">
|
||||||
|
<span class="subject-name">{{ enrollment.subjectName }}</span>
|
||||||
|
<span class="professor">Prof. {{ enrollment.professorName }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="credits">{{ enrollment.credits }} creditos</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if (canEnrollMore()) {
|
||||||
|
<div class="card-actions">
|
||||||
|
<a [routerLink]="['/enrollment', studentId()]" class="btn btn-secondary">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
Inscribir otra materia
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Quick Actions Card -->
|
||||||
|
<section class="card actions-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>
|
||||||
|
<mat-icon>flash_on</mat-icon>
|
||||||
|
Acciones Rapidas
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-grid">
|
||||||
|
<a [routerLink]="['/enrollment', studentId()]" class="action-btn">
|
||||||
|
<mat-icon>edit_note</mat-icon>
|
||||||
|
<span>Gestionar Inscripciones</span>
|
||||||
|
<small>Inscribir o retirar materias</small>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [routerLink]="['/classmates', studentId()]" class="action-btn">
|
||||||
|
<mat-icon>groups</mat-icon>
|
||||||
|
<span>Ver Companeros</span>
|
||||||
|
<small>Conoce a tus companeros de clase</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Program Info Card -->
|
||||||
|
<section class="card info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>
|
||||||
|
<mat-icon>info</mat-icon>
|
||||||
|
Programa de Creditos
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<mat-icon>book</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>10 materias</strong>
|
||||||
|
<span>disponibles en total</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<mat-icon>stars</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>3 creditos</strong>
|
||||||
|
<span>por cada materia</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<mat-icon>check_circle</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Maximo 3 materias</strong>
|
||||||
|
<span>puedes inscribir (9 creditos)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<mat-icon>person</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>5 profesores</strong>
|
||||||
|
<span>cada uno imparte 2 materias</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item warning">
|
||||||
|
<mat-icon>warning</mat-icon>
|
||||||
|
<div>
|
||||||
|
<strong>Restriccion</strong>
|
||||||
|
<span>No puedes tener 2 materias con el mismo profesor</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
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<Student | null>(null);
|
||||||
|
loading = signal(true);
|
||||||
|
error = signal<string | null>(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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue