dead code cleanup and component consolidation

This commit is contained in:
2026-04-08 17:39:03 -07:00
parent e0a34f4c59
commit af4ec3f751
23 changed files with 299 additions and 349 deletions
@@ -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 {
}
@@ -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>
@@ -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 +0,0 @@
/* Inherits all styles from modal-shell via ::ng-deep */
@@ -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);
}
}
}
@@ -1,10 +1,11 @@
<app-modal-shell title="Edit Student" (closed)="closed.emit()"> <app-modal-shell [title]="modalTitle" (closed)="closed.emit()">
<div class="field"> <div class="field">
<label class="field-label">Name</label> <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>
<div class="field"> <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" /> <input class="field-input" type="date" [(ngModel)]="nextIepDate" />
</div> </div>
@@ -14,8 +15,8 @@
<div class="modal-actions"> <div class="modal-actions">
<button class="btn-secondary" (click)="closed.emit()">Cancel</button> <button class="btn-secondary" (click)="closed.emit()">Cancel</button>
<button class="btn-primary" (click)="onSave()" [disabled]="saving()"> <button class="btn-primary" (click)="onSubmit()" [disabled]="isSubmitting() || !identifier.trim()">
{{ saving() ? 'Saving...' : 'Save' }} {{ isSubmitting() ? 'Saving...' : submitLabel }}
</button> </button>
</div> </div>
</app-modal-shell> </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,56 +1,4 @@
:host { @use '../../styles/detail-page';
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;
}
.card-header { .card-header {
display: flex; display: flex;
@@ -70,28 +18,6 @@
padding: 22px; 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 { .date-row {
display: flex; display: flex;
gap: 1.5rem; gap: 1.5rem;
@@ -129,12 +55,6 @@
} }
} }
.actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.run-btn { .run-btn {
background: var(--accent-indigo) !important; background: var(--accent-indigo) !important;
color: #fff !important; color: #fff !important;
@@ -4,6 +4,7 @@ import { Router } from '@angular/router';
import { StudentService } from '../../../shared/services/student.service'; import { StudentService } from '../../../shared/services/student.service';
import { StudentCardDto } from '../../../shared/classes/student-card.dto'; import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentGoalItem } from '../../../shared/classes/student-goal'; import { StudentGoalItem } from '../../../shared/classes/student-goal';
import { toIsoDateString } from '../../../shared/utils/format-date';
interface GoalCheckItem { interface GoalCheckItem {
goalId: string; goalId: string;
@@ -62,10 +63,10 @@ export class StudentProgressReport {
if (!student) return; if (!student) return;
if (student.firstEntryDate) { if (student.firstEntryDate) {
this.fromDate = this.toIsoDate(new Date(student.firstEntryDate)); this.fromDate = toIsoDateString(new Date(student.firstEntryDate));
} }
if (student.lastEntryDate) { 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); const goalsResult = await this.studentService.getGoalsForStudent(this.selectedStudentId);
@@ -145,15 +146,4 @@ export class StudentProgressReport {
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); 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">
<div class="goal-card-header"> <div class="goal-card-header">
<span class="goal-badge">{{ selectedGoal()!.category }} Goal</span> <span class="goal-badge">{{ selectedGoal()!.category }} Goal</span>
<button class="edit-icon" (click)="onEditGoal()" aria-label="Edit goal"> <app-edit-icon (click)="onEditGoal()" ariaLabel="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>
@if (selectedGoal()!.targetCompletionDate) { @if (selectedGoal()!.targetCompletionDate) {
<span class="goal-due">Due {{ formatDate(selectedGoal()!.targetCompletionDate) }}</span> <span class="goal-due">Due {{ formatDate(selectedGoal()!.targetCompletionDate) }}</span>
} }
@@ -81,12 +76,7 @@
<div class="benchmark-card"> <div class="benchmark-card">
<div class="benchmark-header"> <div class="benchmark-header">
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span> <span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
<button class="edit-icon" (click)="onEditBenchmark(b)" aria-label="Edit benchmark"> <app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="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>
</div> </div>
<p class="benchmark-desc">{{ b.benchmark }}</p> <p class="benchmark-desc">{{ b.benchmark }}</p>
</div> </div>
@@ -105,14 +95,16 @@
<div class="event-card"> <div class="event-card">
<div class="event-header"> <div class="event-header">
<span class="event-date">{{ formatDate(ev.createdAt) }}</span> <span class="event-date">{{ formatDate(ev.createdAt) }}</span>
<button class="edit-icon" (click)="onEditEvent(ev)" aria-label="Edit event"> <app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
<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>
</div> </div>
<p class="event-content">{{ ev.content }}</p> <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>
</div> </div>
} }
@@ -124,20 +124,6 @@
color: #333; 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 ─── */
.sub-tabs { .sub-tabs {
display: flex; display: flex;
@@ -265,6 +251,23 @@
color: #333; 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 Buttons ─── */
.add-btn { .add-btn {
padding: 12px; padding: 12px;
@@ -8,12 +8,14 @@ import { StudentFullProfileDto, ProgressEventWithGoalDto, ProgressEventBenchmark
import { GoalModal } from '../goal-modal/goal-modal'; import { GoalModal } from '../goal-modal/goal-modal';
import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal'; import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal';
import { EditEventModal } from '../edit-event-modal/edit-event-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'; type TabView = 'benchmarks' | 'progress';
@Component({ @Component({
selector: 'app-workspace', selector: 'app-workspace',
imports: [GoalModal, EditBenchmarkModal, EditEventModal], imports: [GoalModal, EditBenchmarkModal, EditEventModal, EditIcon],
templateUrl: './workspace.html', templateUrl: './workspace.html',
styleUrl: './workspace.scss', styleUrl: './workspace.scss',
}) })
@@ -177,14 +179,13 @@ export class Workspace {
.map(link => link.benchmarkId); .map(link => link.benchmarkId);
} }
// ************************ Formatting Helpers ********************** getBenchmarksForEvent(progressEventId: string): BenchmarkDto[] {
const ids = this.getBenchmarkIdsForEvent(progressEventId);
formatDate(d: string | Date | null): string { return this.benchmarks().filter(b => ids.includes(b.benchmarkId));
if (!d) return '';
const date = new Date(d);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
} }
formatDate = formatDate;
// ********************** Support Procedures *********************** // ********************** Support Procedures ***********************
private async loadStudentData(studentId: string) { private async loadStudentData(studentId: string) {
@@ -1,12 +1,11 @@
<div class="shell"> <div class="shell">
<!-- Modals --> <!-- Modals -->
@if (showAddStudentModal()) { @if (showStudentModal()) {
<app-add-student-modal (studentCreated)="onStudentCreated($event)" <app-student-modal
(cancelled)="showAddStudentModal.set(false)" /> [student]="showStudentModal() === 'add' ? null : $any(showStudentModal())"
} (studentCreated)="onStudentCreated($event)"
@if (editingStudent()) { (saved)="onStudentSaved()"
<app-edit-student-modal [student]="editingStudent()!" (saved)="onEditStudentSaved()" (closed)="showStudentModal.set(null)" />
(closed)="editingStudent.set(null)" />
} }
<!-- Sidebar --> <!-- Sidebar -->
@@ -43,12 +42,7 @@
IEP: {{ formatDate(s.nextIepDate) }} IEP: {{ formatDate(s.nextIepDate) }}
</div> </div>
</div> </div>
<button class="edit-pencil" (click)="onEditStudent(s, $event)" aria-label="Edit student"> <app-edit-icon (click)="onEditStudent(s, $event)" ariaLabel="Edit student" color="#bbb" />
<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>
</div> </div>
} }
} }
@@ -157,21 +157,6 @@
color: var(--text-dim); 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 ─── */
.sidebar-footer { .sidebar-footer {
padding: 10px 12px; padding: 10px 12px;
@@ -3,12 +3,13 @@ import { RouterLink, RouterOutlet, Router } from '@angular/router';
import { Auth } from '../../../shared/services/auth'; import { Auth } from '../../../shared/services/auth';
import { StudentService } from '../../../shared/services/student.service'; import { StudentService } from '../../../shared/services/student.service';
import { StudentCardDto } from '../../../shared/classes/student-card.dto'; import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { AddStudentModal } from '../../components/add-student-modal/add-student-modal'; import { StudentModal } from '../../components/student-modal/student-modal';
import { EditStudentModal } from '../../components/edit-student-modal/edit-student-modal'; import { EditIcon } from '../../components/edit-icon/edit-icon';
import { formatDate } from '../../../shared/utils/format-date';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
imports: [RouterOutlet, RouterLink, AddStudentModal, EditStudentModal], imports: [RouterOutlet, RouterLink, StudentModal, EditIcon],
templateUrl: './home.html', templateUrl: './home.html',
styleUrl: './home.scss', styleUrl: './home.scss',
}) })
@@ -39,8 +40,7 @@ export class Home {
protected readonly students = signal<StudentCardDto[]>([]); protected readonly students = signal<StudentCardDto[]>([]);
protected readonly selectedStudentId = signal<string | null>(null); protected readonly selectedStudentId = signal<string | null>(null);
protected readonly showAll = signal(false); protected readonly showAll = signal(false);
protected readonly showAddStudentModal = signal(false); protected readonly showStudentModal = signal<StudentCardDto | 'add' | null>(null);
protected readonly editingStudent = signal<StudentCardDto | null>(null);
// Groups students by owner when "All" is active. // Groups students by owner when "All" is active.
protected readonly groupedStudents = computed(() => { protected readonly groupedStudents = computed(() => {
@@ -88,11 +88,11 @@ export class Home {
} }
onAddStudent() { onAddStudent() {
this.showAddStudentModal.set(true); this.showStudentModal.set('add');
} }
onStudentCreated(student: StudentCardDto) { onStudentCreated(student: StudentCardDto) {
this.showAddStudentModal.set(false); this.showStudentModal.set(null);
this.studentService.notifyDataChanged(); this.studentService.notifyDataChanged();
this.selectedStudentId.set(student.studentId); this.selectedStudentId.set(student.studentId);
this.router.navigate(['/students', student.studentId]); this.router.navigate(['/students', student.studentId]);
@@ -100,11 +100,11 @@ export class Home {
onEditStudent(student: StudentCardDto, event: Event) { onEditStudent(student: StudentCardDto, event: Event) {
event.stopPropagation(); event.stopPropagation();
this.editingStudent.set(student); this.showStudentModal.set(student);
} }
onEditStudentSaved() { onStudentSaved() {
this.editingStudent.set(null); this.showStudentModal.set(null);
this.loadStudents(); this.loadStudents();
} }
@@ -113,12 +113,7 @@ export class Home {
this.auth.forceLogout(); this.auth.forceLogout();
} }
// ************************ Formatting Helpers ********************** formatDate = formatDate;
formatDate(d: Date | null): string {
if (!d) return '';
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
// ********************** Support Procedures *********************** // ********************** 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 { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { inject, Injectable, signal } from '@angular/core'; import { inject, Injectable, signal } from '@angular/core';
import { firstValueFrom, Subject } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { ApiResult } from '../classes/api-result'; import { ApiResult } from '../classes/api-result';
import { ResponseResult } from '../classes/auth.models'; import { ResponseResult } from '../classes/auth.models';
@@ -31,10 +31,6 @@ export class StudentService {
// Per-student full profile cache. // Per-student full profile cache.
private readonly profileCache = new Map<string, StudentFullProfileDto>(); 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 *************************** // ************************** Properties ***************************
// ************************ Public Methods ************************* // ************************ 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. // Returns student card summaries for the authenticated user.
// When scope is 'all', returns all students in the program. // 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}`;
}