mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 00:38:44 +00:00
dead code cleanup and component consolidation
This commit is contained in:
@@ -1 +0,0 @@
|
||||
<p>example works!</p>
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Example } from './example';
|
||||
|
||||
describe('Example', () => {
|
||||
let component: Example;
|
||||
let fixture: ComponentFixture<Example>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Example]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Example);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-example',
|
||||
imports: [],
|
||||
templateUrl: './example.html',
|
||||
styleUrl: './example.scss',
|
||||
})
|
||||
export class Example {
|
||||
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<app-modal-shell title="Add Student" (closed)="cancelled.emit()">
|
||||
<div class="field">
|
||||
<label class="field-label">Name</label>
|
||||
<input class="field-input" type="text" [(ngModel)]="form.identifier"
|
||||
placeholder="Initials or other non-personally identifiable label" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Next IEP Date</label>
|
||||
<input class="field-input" type="date" [(ngModel)]="form.nextIepDate" />
|
||||
</div>
|
||||
|
||||
@if (errorMessage()) {
|
||||
<p class="error">{{ errorMessage() }}</p>
|
||||
}
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" (click)="cancelled.emit()">Cancel</button>
|
||||
<button class="btn-primary" (click)="onSubmit()" [disabled]="isSubmitting() || !form.identifier.trim()">
|
||||
{{ isSubmitting() ? 'Saving...' : 'Add Student' }}
|
||||
</button>
|
||||
</div>
|
||||
</app-modal-shell>
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { Component, inject, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ModalShell } from '../modal-shell/modal-shell';
|
||||
import { CreateStudentDto } from '../../../shared/classes/create-student.dto';
|
||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-student-modal',
|
||||
imports: [FormsModule, ModalShell],
|
||||
templateUrl: './add-student-modal.html',
|
||||
styleUrl: './add-student-modal.scss',
|
||||
})
|
||||
export class AddStudentModal {
|
||||
private readonly studentService = inject(StudentService);
|
||||
|
||||
readonly studentCreated = output<StudentCardDto>();
|
||||
readonly cancelled = output<void>();
|
||||
|
||||
protected readonly isSubmitting = signal(false);
|
||||
protected readonly errorMessage = signal<string | null>(null);
|
||||
|
||||
protected form: CreateStudentDto = {
|
||||
identifier: '',
|
||||
programYear: null,
|
||||
enrollmentDate: null,
|
||||
nextIepDate: null,
|
||||
};
|
||||
|
||||
async onSubmit() {
|
||||
if (!this.form.identifier.trim()) return;
|
||||
this.errorMessage.set(null);
|
||||
this.isSubmitting.set(true);
|
||||
|
||||
const result = await this.studentService.createStudent(this.form);
|
||||
this.isSubmitting.set(false);
|
||||
|
||||
if (!result.success) {
|
||||
this.errorMessage.set(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.studentCreated.emit(result.payload!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-icon',
|
||||
imports: [],
|
||||
template: `
|
||||
<button class="edit-icon" [attr.aria-label]="ariaLabel()">
|
||||
<svg [attr.width]="size()" [attr.height]="size()" viewBox="0 0 16 16" fill="none" [attr.stroke]="color()" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
`,
|
||||
styles: [`
|
||||
.edit-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.edit-icon:hover svg {
|
||||
stroke: #555 !important; /* Force hover color since original styles did the same */
|
||||
}
|
||||
:host-context(.student-item) .edit-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
:host-context(.student-item:hover) .edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class EditIcon {
|
||||
readonly size = input<number | string>(14);
|
||||
readonly color = input<string>('#999');
|
||||
readonly ariaLabel = input<string>('Edit');
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
/* Inherits all styles from modal-shell via ::ng-deep */
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { Component, inject, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ModalShell } from '../modal-shell/modal-shell';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-student-modal',
|
||||
imports: [FormsModule, ModalShell],
|
||||
templateUrl: './edit-student-modal.html',
|
||||
styleUrl: './edit-student-modal.scss',
|
||||
})
|
||||
export class EditStudentModal {
|
||||
private readonly studentService = inject(StudentService);
|
||||
|
||||
readonly student = input.required<StudentCardDto>();
|
||||
readonly saved = output<void>();
|
||||
readonly closed = output<void>();
|
||||
|
||||
protected readonly saving = signal(false);
|
||||
protected readonly errorMessage = signal<string | null>(null);
|
||||
|
||||
protected identifier = '';
|
||||
protected nextIepDate = '';
|
||||
|
||||
ngOnInit() {
|
||||
const s = this.student();
|
||||
this.identifier = s.identifier;
|
||||
this.nextIepDate = s.nextIepDate ? new Date(s.nextIepDate).toISOString().split('T')[0] : '';
|
||||
}
|
||||
|
||||
async onSave() {
|
||||
if (!this.identifier.trim()) return;
|
||||
this.saving.set(true);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
const result = await this.studentService.updateStudent(this.student().studentId, {
|
||||
identifier: this.identifier,
|
||||
nextIepDate: this.nextIepDate || null,
|
||||
});
|
||||
|
||||
this.saving.set(false);
|
||||
|
||||
if (result.success) {
|
||||
this.studentService.notifyDataChanged();
|
||||
this.saved.emit();
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -1,10 +1,11 @@
|
||||
<app-modal-shell title="Edit Student" (closed)="closed.emit()">
|
||||
<app-modal-shell [title]="modalTitle" (closed)="closed.emit()">
|
||||
<div class="field">
|
||||
<label class="field-label">Name</label>
|
||||
<input class="field-input" type="text" [(ngModel)]="identifier" />
|
||||
<input class="field-input" type="text" [(ngModel)]="identifier"
|
||||
placeholder="Initials or other non-personally identifiable label" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">IEP Date</label>
|
||||
<label class="field-label">Next IEP Date</label>
|
||||
<input class="field-input" type="date" [(ngModel)]="nextIepDate" />
|
||||
</div>
|
||||
|
||||
@@ -14,8 +15,8 @@
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" (click)="closed.emit()">Cancel</button>
|
||||
<button class="btn-primary" (click)="onSave()" [disabled]="saving()">
|
||||
{{ saving() ? 'Saving...' : 'Save' }}
|
||||
<button class="btn-primary" (click)="onSubmit()" [disabled]="isSubmitting() || !identifier.trim()">
|
||||
{{ isSubmitting() ? 'Saving...' : submitLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</app-modal-shell>
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Component, inject, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ModalShell } from '../modal-shell/modal-shell';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||
import { toIsoDateString } from '../../../shared/utils/format-date';
|
||||
|
||||
@Component({
|
||||
selector: 'app-student-modal',
|
||||
imports: [FormsModule, ModalShell],
|
||||
templateUrl: './student-modal.html',
|
||||
styleUrl: './student-modal.scss',
|
||||
})
|
||||
export class StudentModal {
|
||||
private readonly studentService = inject(StudentService);
|
||||
|
||||
/** Optional: when provided the modal operates in edit mode. */
|
||||
readonly student = input<StudentCardDto | null>(null);
|
||||
|
||||
/** Emits the newly created student (add mode). */
|
||||
readonly studentCreated = output<StudentCardDto>();
|
||||
|
||||
/** Emits when an existing student has been saved (edit mode). */
|
||||
readonly saved = output<void>();
|
||||
|
||||
/** Emits when the modal is dismissed without saving. */
|
||||
readonly closed = output<void>();
|
||||
|
||||
protected readonly isSubmitting = signal(false);
|
||||
protected readonly errorMessage = signal<string | null>(null);
|
||||
|
||||
protected identifier = '';
|
||||
protected nextIepDate = '';
|
||||
|
||||
protected get isEditMode(): boolean {
|
||||
return !!this.student();
|
||||
}
|
||||
|
||||
protected get modalTitle(): string {
|
||||
return this.isEditMode ? 'Edit Student' : 'Add Student';
|
||||
}
|
||||
|
||||
protected get submitLabel(): string {
|
||||
return this.isEditMode ? 'Save' : 'Add Student';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const s = this.student();
|
||||
if (s) {
|
||||
// Edit mode — populate form from the existing student
|
||||
this.identifier = s.identifier;
|
||||
this.nextIepDate = s.nextIepDate ? toIsoDateString(s.nextIepDate) : '';
|
||||
}
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
if (!this.identifier.trim()) return;
|
||||
this.errorMessage.set(null);
|
||||
this.isSubmitting.set(true);
|
||||
|
||||
if (this.isEditMode) {
|
||||
const result = await this.studentService.updateStudent(this.student()!.studentId, {
|
||||
identifier: this.identifier,
|
||||
nextIepDate: this.nextIepDate || null,
|
||||
});
|
||||
this.isSubmitting.set(false);
|
||||
|
||||
if (result.success) {
|
||||
this.studentService.notifyDataChanged();
|
||||
this.saved.emit();
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
} else {
|
||||
const result = await this.studentService.createStudent({
|
||||
identifier: this.identifier,
|
||||
programYear: null,
|
||||
enrollmentDate: null,
|
||||
nextIepDate: this.nextIepDate ? new Date(this.nextIepDate) : null,
|
||||
});
|
||||
this.isSubmitting.set(false);
|
||||
|
||||
if (result.success) {
|
||||
this.studentCreated.emit(result.payload!);
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-81
@@ -1,56 +1,4 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24px 28px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 6px 14px;
|
||||
background: transparent;
|
||||
color: var(--accent-indigo);
|
||||
border: 1px solid var(--accent-indigo);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
background: #EEF2FF;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-xl);
|
||||
max-width: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@use '../../styles/detail-page';
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
@@ -70,28 +18,6 @@
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-muted);
|
||||
border-radius: var(--radius-md);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.date-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
@@ -129,12 +55,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
background: var(--accent-indigo) !important;
|
||||
color: #fff !important;
|
||||
|
||||
+3
-13
@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||
import { toIsoDateString } from '../../../shared/utils/format-date';
|
||||
|
||||
interface GoalCheckItem {
|
||||
goalId: string;
|
||||
@@ -62,10 +63,10 @@ export class StudentProgressReport {
|
||||
if (!student) return;
|
||||
|
||||
if (student.firstEntryDate) {
|
||||
this.fromDate = this.toIsoDate(new Date(student.firstEntryDate));
|
||||
this.fromDate = toIsoDateString(new Date(student.firstEntryDate));
|
||||
}
|
||||
if (student.lastEntryDate) {
|
||||
this.toDate = this.toIsoDate(new Date(student.lastEntryDate));
|
||||
this.toDate = toIsoDateString(new Date(student.lastEntryDate));
|
||||
}
|
||||
|
||||
const goalsResult = await this.studentService.getGoalsForStudent(this.selectedStudentId);
|
||||
@@ -145,15 +146,4 @@ export class StudentProgressReport {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Formats a Date as an ISO date string (yyyy-MM-dd) for use with
|
||||
// the native HTML date input.
|
||||
// *****************************************************************
|
||||
private toIsoDate(date: Date): string {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +48,7 @@
|
||||
<div class="goal-card">
|
||||
<div class="goal-card-header">
|
||||
<span class="goal-badge">{{ selectedGoal()!.category }} Goal</span>
|
||||
<button class="edit-icon" (click)="onEditGoal()" aria-label="Edit goal">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#999" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<app-edit-icon (click)="onEditGoal()" ariaLabel="Edit goal" />
|
||||
@if (selectedGoal()!.targetCompletionDate) {
|
||||
<span class="goal-due">Due {{ formatDate(selectedGoal()!.targetCompletionDate) }}</span>
|
||||
}
|
||||
@@ -81,12 +76,7 @@
|
||||
<div class="benchmark-card">
|
||||
<div class="benchmark-header">
|
||||
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
|
||||
<button class="edit-icon" (click)="onEditBenchmark(b)" aria-label="Edit benchmark">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="#999" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="Edit benchmark" />
|
||||
</div>
|
||||
<p class="benchmark-desc">{{ b.benchmark }}</p>
|
||||
</div>
|
||||
@@ -105,14 +95,16 @@
|
||||
<div class="event-card">
|
||||
<div class="event-header">
|
||||
<span class="event-date">{{ formatDate(ev.createdAt) }}</span>
|
||||
<button class="edit-icon" (click)="onEditEvent(ev)" aria-label="Edit event">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="#bbb" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
|
||||
</div>
|
||||
<p class="event-content">{{ ev.content }}</p>
|
||||
@if (getBenchmarksForEvent(ev.progressEventId).length > 0) {
|
||||
<div class="event-benchmarks">
|
||||
@for (b of getBenchmarksForEvent(ev.progressEventId); track b.benchmarkId) {
|
||||
<span class="benchmark-tag">{{ b.shortName || b.benchmark }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -124,20 +124,6 @@
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover svg {
|
||||
stroke: #555;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Sub Tabs ─── */
|
||||
.sub-tabs {
|
||||
display: flex;
|
||||
@@ -265,6 +251,23 @@
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.event-benchmarks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.benchmark-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #4338CA;
|
||||
background: #EEF2FF;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #C7D2FE;
|
||||
}
|
||||
|
||||
/* ─── Add Buttons ─── */
|
||||
.add-btn {
|
||||
padding: 12px;
|
||||
|
||||
@@ -8,12 +8,14 @@ import { StudentFullProfileDto, ProgressEventWithGoalDto, ProgressEventBenchmark
|
||||
import { GoalModal } from '../goal-modal/goal-modal';
|
||||
import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal';
|
||||
import { EditEventModal } from '../edit-event-modal/edit-event-modal';
|
||||
import { EditIcon } from '../edit-icon/edit-icon';
|
||||
import { formatDate } from '../../../shared/utils/format-date';
|
||||
|
||||
type TabView = 'benchmarks' | 'progress';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace',
|
||||
imports: [GoalModal, EditBenchmarkModal, EditEventModal],
|
||||
imports: [GoalModal, EditBenchmarkModal, EditEventModal, EditIcon],
|
||||
templateUrl: './workspace.html',
|
||||
styleUrl: './workspace.scss',
|
||||
})
|
||||
@@ -177,14 +179,13 @@ export class Workspace {
|
||||
.map(link => link.benchmarkId);
|
||||
}
|
||||
|
||||
// ************************ Formatting Helpers **********************
|
||||
|
||||
formatDate(d: string | Date | null): string {
|
||||
if (!d) return '';
|
||||
const date = new Date(d);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
getBenchmarksForEvent(progressEventId: string): BenchmarkDto[] {
|
||||
const ids = this.getBenchmarkIdsForEvent(progressEventId);
|
||||
return this.benchmarks().filter(b => ids.includes(b.benchmarkId));
|
||||
}
|
||||
|
||||
formatDate = formatDate;
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
|
||||
private async loadStudentData(studentId: string) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<div class="shell">
|
||||
<!-- Modals -->
|
||||
@if (showAddStudentModal()) {
|
||||
<app-add-student-modal (studentCreated)="onStudentCreated($event)"
|
||||
(cancelled)="showAddStudentModal.set(false)" />
|
||||
}
|
||||
@if (editingStudent()) {
|
||||
<app-edit-student-modal [student]="editingStudent()!" (saved)="onEditStudentSaved()"
|
||||
(closed)="editingStudent.set(null)" />
|
||||
@if (showStudentModal()) {
|
||||
<app-student-modal
|
||||
[student]="showStudentModal() === 'add' ? null : $any(showStudentModal())"
|
||||
(studentCreated)="onStudentCreated($event)"
|
||||
(saved)="onStudentSaved()"
|
||||
(closed)="showStudentModal.set(null)" />
|
||||
}
|
||||
|
||||
<!-- Sidebar -->
|
||||
@@ -43,12 +42,7 @@
|
||||
IEP: {{ formatDate(s.nextIepDate) }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="edit-pencil" (click)="onEditStudent(s, $event)" aria-label="Edit student">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#bbb" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<app-edit-icon (click)="onEditStudent(s, $event)" ariaLabel="Edit student" color="#bbb" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,21 +157,6 @@
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.edit-pencil {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
|
||||
.student-item:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Sidebar Footer ─── */
|
||||
.sidebar-footer {
|
||||
padding: 10px 12px;
|
||||
|
||||
@@ -3,12 +3,13 @@ import { RouterLink, RouterOutlet, Router } from '@angular/router';
|
||||
import { Auth } from '../../../shared/services/auth';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||
import { AddStudentModal } from '../../components/add-student-modal/add-student-modal';
|
||||
import { EditStudentModal } from '../../components/edit-student-modal/edit-student-modal';
|
||||
import { StudentModal } from '../../components/student-modal/student-modal';
|
||||
import { EditIcon } from '../../components/edit-icon/edit-icon';
|
||||
import { formatDate } from '../../../shared/utils/format-date';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [RouterOutlet, RouterLink, AddStudentModal, EditStudentModal],
|
||||
imports: [RouterOutlet, RouterLink, StudentModal, EditIcon],
|
||||
templateUrl: './home.html',
|
||||
styleUrl: './home.scss',
|
||||
})
|
||||
@@ -39,8 +40,7 @@ export class Home {
|
||||
protected readonly students = signal<StudentCardDto[]>([]);
|
||||
protected readonly selectedStudentId = signal<string | null>(null);
|
||||
protected readonly showAll = signal(false);
|
||||
protected readonly showAddStudentModal = signal(false);
|
||||
protected readonly editingStudent = signal<StudentCardDto | null>(null);
|
||||
protected readonly showStudentModal = signal<StudentCardDto | 'add' | null>(null);
|
||||
|
||||
// Groups students by owner when "All" is active.
|
||||
protected readonly groupedStudents = computed(() => {
|
||||
@@ -88,11 +88,11 @@ export class Home {
|
||||
}
|
||||
|
||||
onAddStudent() {
|
||||
this.showAddStudentModal.set(true);
|
||||
this.showStudentModal.set('add');
|
||||
}
|
||||
|
||||
onStudentCreated(student: StudentCardDto) {
|
||||
this.showAddStudentModal.set(false);
|
||||
this.showStudentModal.set(null);
|
||||
this.studentService.notifyDataChanged();
|
||||
this.selectedStudentId.set(student.studentId);
|
||||
this.router.navigate(['/students', student.studentId]);
|
||||
@@ -100,11 +100,11 @@ export class Home {
|
||||
|
||||
onEditStudent(student: StudentCardDto, event: Event) {
|
||||
event.stopPropagation();
|
||||
this.editingStudent.set(student);
|
||||
this.showStudentModal.set(student);
|
||||
}
|
||||
|
||||
onEditStudentSaved() {
|
||||
this.editingStudent.set(null);
|
||||
onStudentSaved() {
|
||||
this.showStudentModal.set(null);
|
||||
this.loadStudents();
|
||||
}
|
||||
|
||||
@@ -113,12 +113,7 @@ export class Home {
|
||||
this.auth.forceLogout();
|
||||
}
|
||||
|
||||
// ************************ Formatting Helpers **********************
|
||||
|
||||
formatDate(d: Date | null): string {
|
||||
if (!d) return '';
|
||||
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
formatDate = formatDate;
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24px 28px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 6px 14px;
|
||||
background: transparent;
|
||||
color: var(--accent-indigo);
|
||||
border: 1px solid var(--accent-indigo);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
background: #EEF2FF;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-xl);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-muted);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-textarea {
|
||||
resize: vertical;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 13px;
|
||||
color: #dc2626;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { firstValueFrom, Subject } from 'rxjs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ApiResult } from '../classes/api-result';
|
||||
import { ResponseResult } from '../classes/auth.models';
|
||||
@@ -31,10 +31,6 @@ export class StudentService {
|
||||
// Per-student full profile cache.
|
||||
private readonly profileCache = new Map<string, StudentFullProfileDto>();
|
||||
|
||||
// Emits targeted label updates for sidebar nodes without a full rebuild.
|
||||
private readonly _sidebarLabelUpdate = new Subject<{ routerLink: string[]; label: string }>();
|
||||
readonly sidebarLabelUpdate$ = this._sidebarLabelUpdate.asObservable();
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
// ************************ Public Methods *************************
|
||||
@@ -81,14 +77,6 @@ export class StudentService {
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Emits a targeted sidebar label update for a specific node,
|
||||
// avoiding the full tree rebuild that notifyDataChanged triggers.
|
||||
// *****************************************************************
|
||||
updateSidebarLabel(routerLink: string[], label: string) {
|
||||
this._sidebarLabelUpdate.next({ routerLink, label });
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns student card summaries for the authenticated user.
|
||||
// When scope is 'all', returns all students in the program.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export function formatDate(d: string | Date | null): string {
|
||||
if (!d) return '';
|
||||
const date = new Date(d);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
export function toIsoDateString(d: string | Date): string {
|
||||
const date = new Date(d);
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
Reference in New Issue
Block a user