mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 01:47:41 +00:00
Latest
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { Login } from './shared/pages/login/login';
|
||||
import { Register } from './shared/pages/register/register';
|
||||
import { PlatformService } from './shared/services/platform.service';
|
||||
import { authGuard } from './shared/guards/auth.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'login', component: Login },
|
||||
{ path: 'register', component: Register },
|
||||
{
|
||||
path: '',
|
||||
canMatch: [() => inject(PlatformService).formFactor() === 'mobile'],
|
||||
|
||||
@@ -19,6 +19,29 @@
|
||||
<input class="field-input" type="date" [(ngModel)]="form.targetCompletionDate" />
|
||||
</div>
|
||||
|
||||
@if (isEditMode) {
|
||||
<hr class="close-divider" />
|
||||
<div class="field">
|
||||
<label class="field-label">Close Date</label>
|
||||
<input class="field-input" type="date" [(ngModel)]="closeDate" />
|
||||
</div>
|
||||
@if (closeDate) {
|
||||
<div class="field">
|
||||
<label class="field-check">
|
||||
<input type="checkbox" [(ngModel)]="achieved" />
|
||||
Goal achieved
|
||||
</label>
|
||||
</div>
|
||||
@if (!achieved) {
|
||||
<div class="field">
|
||||
<label class="field-label">Close Notes</label>
|
||||
<textarea class="field-input field-textarea" [(ngModel)]="closeNotes"
|
||||
placeholder="Reason the goal was closed without being achieved..."></textarea>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if (errorMessage()) {
|
||||
<p class="error">{{ errorMessage() }}</p>
|
||||
}
|
||||
|
||||
@@ -1 +1,24 @@
|
||||
/* Inherits all styles from modal-shell via ::ng-deep */
|
||||
|
||||
:host ::ng-deep {
|
||||
.close-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 6px 0 14px;
|
||||
}
|
||||
|
||||
.field-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ export class GoalModal {
|
||||
targetCompletionDate: null,
|
||||
};
|
||||
|
||||
// Close-goal fields — only used in edit mode.
|
||||
protected closeDate: string | null = null;
|
||||
protected achieved = false;
|
||||
protected closeNotes = '';
|
||||
|
||||
protected get isEditMode(): boolean {
|
||||
return !!this.goal();
|
||||
}
|
||||
@@ -66,6 +71,11 @@ export class GoalModal {
|
||||
this.form.targetCompletionDate = existing.targetCompletionDate
|
||||
? existing.targetCompletionDate.substring(0, 10)
|
||||
: null;
|
||||
this.closeDate = existing.closeDate
|
||||
? existing.closeDate.substring(0, 10)
|
||||
: null;
|
||||
this.achieved = existing.achieved ?? false;
|
||||
this.closeNotes = existing.closeNotes ?? '';
|
||||
} else {
|
||||
// Add mode — pre-fill target date from IEP if available
|
||||
const iepDate = this.nextIepDate?.();
|
||||
@@ -89,6 +99,9 @@ export class GoalModal {
|
||||
description: this.form.description,
|
||||
baseline: this.form.baseline,
|
||||
targetCompletionDate: this.form.targetCompletionDate,
|
||||
closeDate: this.closeDate || null,
|
||||
achieved: this.closeDate ? this.achieved : null,
|
||||
closeNotes: this.closeDate && !this.achieved ? this.closeNotes || null : null,
|
||||
},
|
||||
);
|
||||
this.isSubmitting.set(false);
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
[message]="'Delete \u0022' + (deletingBenchmark()!.shortName || deletingBenchmark()!.benchmark) + '\u0022? This cannot be undone.'"
|
||||
confirmLabel="Delete"
|
||||
[destructive]="true"
|
||||
[doubleConfirm]="true"
|
||||
(confirmed)="onDeleteBenchmarkConfirmed()"
|
||||
(closed)="showDeleteBenchmarkConfirm.set(false)" />
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
<button class="goal-tab add-goal" (click)="onAddGoal()">+ Goal</button>
|
||||
@for (g of goals(); track g.goalId) {
|
||||
<button class="goal-tab" [class.active]="selectedGoalId() === g.goalId || (selectedGoal()?.goalId === g.goalId)"
|
||||
[class.closed]="!!g.closeDate"
|
||||
(click)="onSelectGoal(g.goalId)">
|
||||
{{ g.category }}
|
||||
</button>
|
||||
@@ -98,6 +100,11 @@
|
||||
@if (selectedGoal()!.targetCompletionDate) {
|
||||
<span class="goal-due">Due {{ formatDate(selectedGoal()!.targetCompletionDate) }}</span>
|
||||
}
|
||||
@if (selectedGoal()!.closeDate) {
|
||||
<span class="goal-status" [class.achieved]="selectedGoal()!.achieved">
|
||||
{{ selectedGoal()!.achieved ? '✓ Achieved' : 'Closed' }}
|
||||
</span>
|
||||
}
|
||||
<button class="delete-goal-btn" (click)="onDeleteGoal()" aria-label="Delete goal" title="Delete goal">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
|
||||
@@ -137,6 +137,28 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.goal-status {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #FEF3C7;
|
||||
color: #92400E;
|
||||
}
|
||||
|
||||
.goal-status.achieved {
|
||||
background: #D1FAE5;
|
||||
color: #065F46;
|
||||
}
|
||||
|
||||
.goal-tab.closed {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.goal-tab.closed.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-goal-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Home } from './pages/home/home';
|
||||
import { Workspace } from './components/workspace/workspace';
|
||||
import { Reports } from './components/reports/reports';
|
||||
import { StudentProgressReport } from './components/student-progress-report/student-progress-report';
|
||||
import { Admin } from './pages/admin/admin';
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
@@ -14,6 +15,7 @@ export default [
|
||||
{ path: 'students/:studentId/goals/:goalId', component: Workspace },
|
||||
{ path: 'reports', component: Reports },
|
||||
{ path: 'reports/student-progress', component: StudentProgressReport },
|
||||
{ path: 'admin', component: Admin },
|
||||
],
|
||||
},
|
||||
] satisfies Routes;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<div class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Administration</h1>
|
||||
</div>
|
||||
|
||||
@if (error()) {
|
||||
<p class="error">{{ error() }}</p>
|
||||
}
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab" [class.active]="activeTab() === 'programs'" (click)="onSwitchTab('programs')">Programs</button>
|
||||
<button class="tab" [class.active]="activeTab() === 'users'" (click)="onSwitchTab('users')">Users</button>
|
||||
</div>
|
||||
|
||||
<!-- Programs Tab -->
|
||||
@if (activeTab() === 'programs') {
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<button class="action-btn" (click)="onAddProgram()">+ Add Program</button>
|
||||
</div>
|
||||
|
||||
@if (programs().length === 0) {
|
||||
<p class="empty">No programs yet. Create your first program to get started.</p>
|
||||
} @else {
|
||||
<div class="list">
|
||||
@for (program of programs(); track program.programId) {
|
||||
<div class="list-item">
|
||||
<div class="list-item-info">
|
||||
<div class="list-item-name">{{ program.name }}</div>
|
||||
@if (program.description) {
|
||||
<div class="list-item-meta">{{ program.description }}</div>
|
||||
}
|
||||
</div>
|
||||
<button class="edit-btn" (click)="onEditProgram(program)">Edit</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Users Tab -->
|
||||
@if (activeTab() === 'users') {
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<button class="action-btn" (click)="onAddUser()">+ Add User</button>
|
||||
</div>
|
||||
|
||||
@if (users().length === 0) {
|
||||
<p class="empty">No users yet.</p>
|
||||
} @else {
|
||||
<div class="list">
|
||||
@for (user of users(); track user.userId + user.programId) {
|
||||
<div class="list-item">
|
||||
<div class="list-item-info">
|
||||
<div class="list-item-name">{{ user.name }}</div>
|
||||
<div class="list-item-meta">{{ user.email }} · {{ user.roleName }} · {{ user.programName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Database Section -->
|
||||
<div class="section-divider"></div>
|
||||
<h2 class="section-heading">Database</h2>
|
||||
<div class="backup-row">
|
||||
<button class="action-btn backup-btn" (click)="onBackupDatabase()" [disabled]="backingUp()">
|
||||
@if (backingUp()) {
|
||||
Backing up…
|
||||
} @else {
|
||||
Back Up Database
|
||||
}
|
||||
</button>
|
||||
@if (backupSuccess()) {
|
||||
<span class="backup-success">{{ backupSuccess() }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Program Modal -->
|
||||
@if (showProgramModal()) {
|
||||
<app-modal-shell [title]="editingProgram() ? 'Edit Program' : 'New Program'" (closed)="showProgramModal.set(false)">
|
||||
<form (ngSubmit)="onSaveProgram()" class="modal-form">
|
||||
<label>
|
||||
Program Name
|
||||
<input type="text" [(ngModel)]="programName" name="programName" required />
|
||||
</label>
|
||||
<label>
|
||||
Description
|
||||
<textarea [(ngModel)]="programDescription" name="programDescription" rows="3"></textarea>
|
||||
</label>
|
||||
<button type="submit">{{ editingProgram() ? 'Save' : 'Create' }}</button>
|
||||
</form>
|
||||
</app-modal-shell>
|
||||
}
|
||||
|
||||
<!-- User Modal -->
|
||||
@if (showUserModal()) {
|
||||
<app-modal-shell title="New User" (closed)="showUserModal.set(false)">
|
||||
<form (ngSubmit)="onSaveUser()" class="modal-form">
|
||||
<label>
|
||||
Name
|
||||
<input type="text" [(ngModel)]="userName" name="userName" required />
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" [(ngModel)]="userEmail" name="userEmail" required />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" [(ngModel)]="userPassword" name="userPassword" required />
|
||||
</label>
|
||||
<label>
|
||||
Program
|
||||
<select [(ngModel)]="userProgramId" name="userProgramId" required>
|
||||
<option value="" disabled>Select a program</option>
|
||||
@for (p of programs(); track p.programId) {
|
||||
<option [value]="p.programId">{{ p.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Role
|
||||
<select [(ngModel)]="userRoleId" name="userRoleId" required>
|
||||
<option value="" disabled>Select a role</option>
|
||||
@for (r of roles(); track r.roleId) {
|
||||
<option [value]="r.roleId">{{ r.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Create User</button>
|
||||
</form>
|
||||
</app-modal-shell>
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
.admin-page {
|
||||
padding: 24px 32px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.admin-header h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
|
||||
&.active {
|
||||
background: #EEF2FF;
|
||||
color: #4338CA;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-faint);
|
||||
font-size: 14px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e5e0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.list-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-item-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-item-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
background: none;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
color: #555;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&:focus {
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border-top: 1px solid #e5e5e0;
|
||||
margin: 28px 0 20px;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.backup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.backup-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.backup-success {
|
||||
font-size: 13px;
|
||||
color: #16a34a;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AdminService, ProgramDto, DistrictUserDto, RoleDto } from '../../../shared/services/admin.service';
|
||||
import { ModalShell } from '../../components/modal-shell/modal-shell';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
imports: [FormsModule, ModalShell],
|
||||
templateUrl: './admin.html',
|
||||
styleUrl: './admin.scss',
|
||||
})
|
||||
export class Admin {
|
||||
|
||||
// ************************** Constructor **************************
|
||||
|
||||
constructor() {
|
||||
this.loadAll();
|
||||
}
|
||||
|
||||
// ************************** Declarations *************************
|
||||
|
||||
private readonly adminService = inject(AdminService);
|
||||
|
||||
protected readonly activeTab = signal<'programs' | 'users'>('programs');
|
||||
protected readonly programs = signal<ProgramDto[]>([]);
|
||||
protected readonly users = signal<DistrictUserDto[]>([]);
|
||||
protected readonly roles = signal<RoleDto[]>([]);
|
||||
protected readonly error = signal<string | null>(null);
|
||||
|
||||
// Program modal
|
||||
protected readonly showProgramModal = signal(false);
|
||||
protected readonly editingProgram = signal<ProgramDto | null>(null);
|
||||
protected programName = '';
|
||||
protected programDescription = '';
|
||||
|
||||
// User modal
|
||||
protected readonly showUserModal = signal(false);
|
||||
protected userName = '';
|
||||
protected userEmail = '';
|
||||
protected userPassword = '';
|
||||
protected userProgramId = '';
|
||||
protected userRoleId = '';
|
||||
|
||||
// Backup
|
||||
protected readonly backingUp = signal(false);
|
||||
protected readonly backupSuccess = signal<string | null>(null);
|
||||
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
onSwitchTab(tab: 'programs' | 'users') {
|
||||
this.activeTab.set(tab);
|
||||
}
|
||||
|
||||
// --- Programs ---
|
||||
|
||||
onAddProgram() {
|
||||
this.editingProgram.set(null);
|
||||
this.programName = '';
|
||||
this.programDescription = '';
|
||||
this.showProgramModal.set(true);
|
||||
}
|
||||
|
||||
onEditProgram(program: ProgramDto) {
|
||||
this.editingProgram.set(program);
|
||||
this.programName = program.name;
|
||||
this.programDescription = program.description || '';
|
||||
this.showProgramModal.set(true);
|
||||
}
|
||||
|
||||
async onSaveProgram() {
|
||||
this.error.set(null);
|
||||
const editing = this.editingProgram();
|
||||
|
||||
if (editing) {
|
||||
const result = await this.adminService.updateProgram(editing.programId, this.programName, this.programDescription);
|
||||
if (!result.success) {
|
||||
this.error.set(result.message);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const result = await this.adminService.createProgram(this.programName, this.programDescription);
|
||||
if (!result.success) {
|
||||
this.error.set(result.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.showProgramModal.set(false);
|
||||
this.programs.set(await this.adminService.getPrograms());
|
||||
}
|
||||
|
||||
// --- Users ---
|
||||
|
||||
onAddUser() {
|
||||
this.userName = '';
|
||||
this.userEmail = '';
|
||||
this.userPassword = '';
|
||||
this.userProgramId = '';
|
||||
this.userRoleId = '';
|
||||
this.showUserModal.set(true);
|
||||
}
|
||||
|
||||
async onSaveUser() {
|
||||
this.error.set(null);
|
||||
const result = await this.adminService.createUser(
|
||||
this.userEmail, this.userName, this.userPassword,
|
||||
this.userProgramId, this.userRoleId
|
||||
);
|
||||
if (!result.success) {
|
||||
this.error.set(result.message);
|
||||
return;
|
||||
}
|
||||
this.showUserModal.set(false);
|
||||
this.users.set(await this.adminService.getUsers());
|
||||
}
|
||||
|
||||
// --- Backup ---
|
||||
|
||||
async onBackupDatabase() {
|
||||
this.backingUp.set(true);
|
||||
this.backupSuccess.set(null);
|
||||
this.error.set(null);
|
||||
try {
|
||||
await this.adminService.backupDatabase();
|
||||
this.backupSuccess.set('Backup downloaded successfully.');
|
||||
} catch {
|
||||
this.error.set('Database backup failed. Please try again.');
|
||||
} finally {
|
||||
this.backingUp.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
|
||||
private async loadAll() {
|
||||
this.programs.set(await this.adminService.getPrograms());
|
||||
this.users.set(await this.adminService.getUsers());
|
||||
this.roles.set(await this.adminService.getRoles());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
|
||||
<div class="sidebar-nav">
|
||||
<a class="nav-link" routerLink="/reports">Reports</a>
|
||||
@if (auth.user()?.role === 'district_admin' || auth.user()?.role === 'super_admin') {
|
||||
<a class="nav-link" routerLink="/admin">Admin</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sidebar-bottom">
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
{{ loading() ? 'Signing in...' : 'Sign in' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a class="link-button" routerLink="/register">Register your school district</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Auth } from '../../services/auth';
|
||||
import { describeHttpError } from '../../classes/http-errors';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, RouterLink],
|
||||
templateUrl: './login.html',
|
||||
styleUrl: './login.css',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
@if (success()) {
|
||||
<div class="card">
|
||||
<h2>Registration Complete</h2>
|
||||
<p class="subtitle">Your district has been created. You can now log in.</p>
|
||||
<a class="link-button" routerLink="/login">Go to Login</a>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="card">
|
||||
<h2>Register Your School District</h2>
|
||||
<p class="subtitle">Create your account and set up your school district.</p>
|
||||
|
||||
@if (error()) {
|
||||
<p class="error">{{ error() }}</p>
|
||||
}
|
||||
|
||||
<form (ngSubmit)="onRegister()">
|
||||
<fieldset>
|
||||
<legend>Your Details</legend>
|
||||
|
||||
<label>
|
||||
Name
|
||||
<input type="text" [(ngModel)]="name" name="name" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Email
|
||||
<input type="email" [(ngModel)]="email" name="email" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Password
|
||||
<input type="password" [(ngModel)]="password" name="password" required />
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>District Details</legend>
|
||||
|
||||
<label>
|
||||
District Name
|
||||
<input type="text" [(ngModel)]="districtName" name="districtName" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Contact Email (optional)
|
||||
<input type="email" [(ngModel)]="districtContactEmail" name="districtContactEmail" />
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Your First Program</legend>
|
||||
|
||||
<label>
|
||||
Program Name
|
||||
<input type="text" [(ngModel)]="programName" name="programName" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Description (optional)
|
||||
<textarea [(ngModel)]="programDescription" name="programDescription" rows="2"></textarea>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit" [disabled]="loading()">
|
||||
{{ loading() ? 'Creating...' : 'Create District & Account' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a class="link-button" routerLink="/login">Already have an account? Sign in</a>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Api } from '../../services/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
imports: [FormsModule, RouterLink],
|
||||
templateUrl: './register.html',
|
||||
styleUrl: './register.css',
|
||||
})
|
||||
export class Register {
|
||||
|
||||
// ************************** Constructor **************************
|
||||
|
||||
private readonly api = inject(Api);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
// ************************** Declarations *************************
|
||||
|
||||
name = '';
|
||||
email = '';
|
||||
password = '';
|
||||
districtName = '';
|
||||
districtContactEmail = '';
|
||||
programName = '';
|
||||
programDescription = '';
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
protected readonly loading = signal(false);
|
||||
protected readonly error = signal<string | null>(null);
|
||||
protected readonly success = signal(false);
|
||||
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
onRegister() {
|
||||
this.error.set(null);
|
||||
this.loading.set(true);
|
||||
|
||||
this.api.register({
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
name: this.name,
|
||||
districtName: this.districtName,
|
||||
districtContactEmail: this.districtContactEmail || undefined,
|
||||
programName: this.programName,
|
||||
programDescription: this.programDescription || undefined,
|
||||
}).subscribe({
|
||||
next: (res) => {
|
||||
this.loading.set(false);
|
||||
if (res.success) {
|
||||
this.success.set(true);
|
||||
} else {
|
||||
this.error.set(res.message);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
this.error.set('An unexpected error occurred. Please try again.');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,12 @@ export class Api {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Self-service registration — creates a new district + program + user
|
||||
register(request: { email: string; password: string; name: string; districtName: string; districtContactEmail?: string; programName: string; programDescription?: string }): Observable<ResponseResult<object>> {
|
||||
return this.http.post<ResponseResult<object>>(
|
||||
`${this.base}/api/Auth/Register`,
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user