mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 12:17:35 +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">
|
<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
-81
@@ -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;
|
||||||
|
|||||||
+3
-13
@@ -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}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user