diff --git a/src/frontend/src/app/app.component.ts b/src/frontend/src/app/app.component.ts index f3e8766..2096d7d 100644 --- a/src/frontend/src/app/app.component.ts +++ b/src/frontend/src/app/app.component.ts @@ -23,12 +23,15 @@ import { ConnectivityOverlayComponent } from '@shared/index';
@if (authService.isAuthenticated()) {
-
+ +
+ Demo + admin · admin123 +
`, @@ -217,6 +222,59 @@ import { NotificationService } from '@core/services'; margin-top: 0.5rem; font-size: 0.875rem; } + + .demo-hint { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 1.5rem; + padding: 0.625rem 1rem; + background: rgba(0, 0, 0, 0.02); + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + animation: fadeIn 0.6s ease-out 0.3s both; + } + + .demo-hint:hover { + background: rgba(0, 0, 0, 0.04); + } + + .demo-hint:active { + transform: scale(0.98); + } + + .demo-label { + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.02em; + text-transform: uppercase; + color: #86868b; + background: rgba(0, 0, 0, 0.05); + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + } + + .demo-credentials { + font-family: 'SF Mono', SFMono-Regular, ui-monospace, monospace; + font-size: 0.75rem; + font-weight: 400; + letter-spacing: -0.01em; + color: #86868b; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } `], }) export class LoginComponent { @@ -238,6 +296,10 @@ export class LoginComponent { return !!(control?.invalid && control?.touched); } + fillDemoCredentials(): void { + this.form.patchValue({ username: 'admin', password: 'admin123' }); + } + onSubmit(): void { if (this.form.invalid) { this.form.markAllAsTouched(); @@ -254,7 +316,8 @@ export class LoginComponent { this.loading.set(false); if (response.success) { this.notification.success('Bienvenido!'); - this.router.navigate(['/dashboard']); + const redirectPath = response.user?.role === 'Admin' ? '/admin' : '/dashboard'; + this.router.navigate([redirectPath]); } else { this.serverError.set(response.error ?? 'Error al iniciar sesion'); } 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 index 37dcb90..2e41819 100644 --- 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 @@ -119,8 +119,8 @@ interface Student { groups - Ver Companeros - Conoce a tus companeros de clase + Ver Compañeros + Conoce a tus compañeros de clase diff --git a/src/frontend/src/app/features/students/pages/student-form/student-form.component.ts b/src/frontend/src/app/features/students/pages/student-form/student-form.component.ts index 86ff422..0e5bdf0 100644 --- a/src/frontend/src/app/features/students/pages/student-form/student-form.component.ts +++ b/src/frontend/src/app/features/students/pages/student-form/student-form.component.ts @@ -7,6 +7,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { StudentService, NotificationService, ErrorHandlerService } from '@core/services'; import { LoadingSpinnerComponent } from '@shared/index'; +import { CreateStudentWithActivationPayload } from '@core/models'; @Component({ selector: 'app-student-form', @@ -44,7 +45,7 @@ import { LoadingSpinnerComponent } from '@shared/index'; type="text" class="input" formControlName="name" - placeholder="Ej: Juan Pérez García" + placeholder="Ej: Juan Perez Garcia" [class.error]="showError('name')" data-testid="input-name" > @@ -60,7 +61,7 @@ import { LoadingSpinnerComponent } from '@shared/index';
- + } @@ -109,6 +110,61 @@ import { LoadingSpinnerComponent } from '@shared/index';
} + + @if (showActivationModal()) { + + } `, styles: [` .page { @@ -165,6 +221,155 @@ import { LoadingSpinnerComponent } from '@shared/index'; @keyframes spin { to { transform: rotate(360deg); } } + + /* Modal Styles */ + .modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s ease; + } + + .modal-content { + background: white; + border-radius: 1rem; + padding: 2rem; + width: 100%; + max-width: 480px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + animation: slideUp 0.3s ease; + } + + @keyframes slideUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } + } + + .modal-header { + text-align: center; + margin-bottom: 1.5rem; + } + + .modal-header h2 { + font-size: 1.5rem; + font-weight: 600; + margin: 0.5rem 0; + color: #1a1a2e; + } + + .modal-header p { + color: #6b7280; + margin: 0; + } + + .success-icon { + font-size: 48px; + width: 48px; + height: 48px; + color: #22c55e; + } + + .activation-info { + background: #f9fafb; + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1.5rem; + } + + .info-row { + display: flex; + justify-content: space-between; + padding: 0.25rem 0; + } + + .info-row .label { + color: #6b7280; + font-size: 0.875rem; + } + + .info-row .value { + font-weight: 500; + color: #1a1a2e; + } + + .activation-code-section, + .activation-url-section { + margin-bottom: 1rem; + } + + .activation-code-section label, + .activation-url-section label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.5rem; + } + + .code-display, + .url-display { + display: flex; + align-items: center; + gap: 0.5rem; + background: #1a1a2e; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + } + + .code-display code, + .url-display code { + flex: 1; + font-family: monospace; + font-size: 1.125rem; + font-weight: 600; + color: #22c55e; + letter-spacing: 0.05em; + word-break: break-all; + } + + .url-display code { + font-size: 0.75rem; + } + + .copy-btn { + background: transparent; + border: none; + color: #9ca3af; + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + transition: color 0.2s; + } + + .copy-btn:hover { + color: white; + } + + .expiration-warning { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.2); + border-radius: 0.5rem; + color: #d97706; + font-size: 0.875rem; + margin-bottom: 1.5rem; + } + + .modal-actions { + display: flex; + justify-content: center; + } `], }) export class StudentFormComponent { @@ -185,6 +390,8 @@ export class StudentFormComponent { loadingStudent = signal(false); saving = signal(false); serverError = signal(null); + showActivationModal = signal(false); + activationData = signal(null); constructor() { // Use effect to react when route param 'id' becomes available @@ -232,32 +439,77 @@ export class StudentFormComponent { this.serverError.set(null); const studentId = this.id(); - const operation = studentId - ? this.studentService.updateStudent(parseInt(studentId, 10), this.form.value) - : this.studentService.createStudent(this.form.value); - const action = studentId ? 'actualizar' : 'crear'; + if (studentId) { + // Update existing student + this.studentService.updateStudent(parseInt(studentId, 10), this.form.value).subscribe({ + next: (result) => { + this.saving.set(false); + if (result.student) { + this.notification.success('Estudiante actualizado correctamente'); + this.router.navigate(['/students']); + } else if (result.errors?.length) { + this.serverError.set(result.errors.join('. ')); + } else { + this.serverError.set('No se pudo actualizar el estudiante. Intenta nuevamente.'); + } + }, + error: (error) => { + this.saving.set(false); + const errorInfo = this.errorHandler.handle(error, 'StudentForm.actualizar'); + this.serverError.set(errorInfo.userMessage); + }, + }); + } else { + // Create new student - show activation modal + this.studentService.createStudent(this.form.value).subscribe({ + next: (result) => { + this.saving.set(false); + if (result.student && result.activationCode) { + this.activationData.set(result); + this.showActivationModal.set(true); + } else if (result.errors?.length) { + this.serverError.set(result.errors.join('. ')); + } else { + this.serverError.set('No se pudo crear el estudiante. Intenta nuevamente.'); + } + }, + error: (error) => { + this.saving.set(false); + const errorInfo = this.errorHandler.handle(error, 'StudentForm.crear'); + this.serverError.set(errorInfo.userMessage); + }, + }); + } + } - operation.subscribe({ - next: (result) => { - this.saving.set(false); - if (result.student) { - this.notification.success( - studentId ? 'Estudiante actualizado correctamente' : 'Estudiante creado correctamente' - ); - this.router.navigate(['/students']); - } else if (result.errors?.length) { - // Errores de validación del backend - this.serverError.set(result.errors.join('. ')); - } else { - this.serverError.set(`No se pudo ${action} el estudiante. Intenta nuevamente.`); - } - }, - error: (error) => { - this.saving.set(false); - const errorInfo = this.errorHandler.handle(error, `StudentForm.${action}`); - this.serverError.set(errorInfo.userMessage); - }, + copyCode(): void { + const code = this.activationData()?.activationCode; + if (code) { + navigator.clipboard.writeText(code); + this.notification.success('Codigo copiado al portapapeles'); + } + } + + copyUrl(): void { + const url = this.activationData()?.activationUrl; + if (url) { + navigator.clipboard.writeText(url); + this.notification.success('URL copiada al portapapeles'); + } + } + + formatExpiration(expiresAt?: string): string { + if (!expiresAt) return ''; + const date = new Date(expiresAt); + return date.toLocaleString('es-CO', { + dateStyle: 'medium', + timeStyle: 'short', }); } + + closeModal(): void { + this.showActivationModal.set(false); + this.router.navigate(['/students']); + } }