mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 09:57:37 +00:00
Added persistent prompt to student progress report
This commit is contained in:
+16
@@ -45,6 +45,22 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label-row">
|
||||
<label class="field-label" for="prompt">Prompt</label>
|
||||
@if (promptSaved()) {
|
||||
<span class="save-indicator">✓ Saved</span>
|
||||
}
|
||||
</div>
|
||||
<textarea
|
||||
id="prompt"
|
||||
class="field-input prompt-textarea"
|
||||
rows="6"
|
||||
[(ngModel)]="promptText"
|
||||
(ngModelChange)="onPromptChange()">
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
class="toolbar-btn run-btn"
|
||||
|
||||
+31
@@ -55,6 +55,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
.field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.field-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.save-indicator {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--accent-green, #22c55e);
|
||||
animation: fadeIn 0.2s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.prompt-textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
background: var(--accent-indigo) !important;
|
||||
color: #fff !important;
|
||||
|
||||
+57
-5
@@ -1,9 +1,9 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { Component, inject, signal, OnDestroy } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
import { ReportPromptService } from '../../../shared/services/report-prompt.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 {
|
||||
@@ -18,29 +18,41 @@ interface GoalCheckItem {
|
||||
templateUrl: './student-progress-report.html',
|
||||
styleUrl: './student-progress-report.scss',
|
||||
})
|
||||
export class StudentProgressReport {
|
||||
export class StudentProgressReport implements OnDestroy {
|
||||
|
||||
// ************************** Constructor **************************
|
||||
|
||||
constructor() {
|
||||
this.loadStudents();
|
||||
this.loadPrompt();
|
||||
}
|
||||
|
||||
// ************************** Declarations *************************
|
||||
|
||||
private readonly router = inject(Router);
|
||||
private readonly studentService = inject(StudentService);
|
||||
private readonly reportPromptService = inject(ReportPromptService);
|
||||
protected readonly students = signal<StudentCardDto[]>([]);
|
||||
protected readonly goalItems = signal<GoalCheckItem[]>([]);
|
||||
protected readonly running = signal(false);
|
||||
protected readonly promptSaved = signal(false);
|
||||
protected selectedStudentId = '';
|
||||
protected fromDate = '';
|
||||
protected toDate = '';
|
||||
protected promptText = '';
|
||||
private promptId = '';
|
||||
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private savedTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
// ************************ Public Methods *************************
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||
if (this.savedTimer) clearTimeout(this.savedTimer);
|
||||
}
|
||||
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
onBack() {
|
||||
@@ -88,6 +100,26 @@ export class StudentProgressReport {
|
||||
);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Debounces prompt changes and auto-saves after 1 second of
|
||||
// inactivity. Shows a brief "Saved" indicator on success.
|
||||
// *****************************************************************
|
||||
onPromptChange() {
|
||||
this.promptSaved.set(false);
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||
|
||||
if (!this.promptId) return;
|
||||
|
||||
this.debounceTimer = setTimeout(async () => {
|
||||
const result = await this.reportPromptService.updatePrompt(this.promptId, this.promptText);
|
||||
if (result.success) {
|
||||
this.promptSaved.set(true);
|
||||
if (this.savedTimer) clearTimeout(this.savedTimer);
|
||||
this.savedTimer = setTimeout(() => this.promptSaved.set(false), 3000);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Calls the API to generate the markdown report, passing only
|
||||
// the checked goal IDs, and triggers a browser download.
|
||||
@@ -129,14 +161,34 @@ export class StudentProgressReport {
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Triggers a browser download of the given markdown content.
|
||||
// Loads the prompt for 'progressreport' from the API.
|
||||
// *****************************************************************
|
||||
private async loadPrompt() {
|
||||
const result = await this.reportPromptService.getByReportname('progressreport');
|
||||
if (result.success && result.payload) {
|
||||
this.promptId = result.payload.reportPromptId;
|
||||
this.promptText = result.payload.prompt;
|
||||
} else {
|
||||
console.error('[loadPrompt] Failed to load prompt:', result.message);
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Triggers a browser download of the given markdown content,
|
||||
// prepending the prompt text at the top of the file.
|
||||
// *****************************************************************
|
||||
private downloadMarkdown(content: string) {
|
||||
const student = this.students().find(s => s.studentId === this.selectedStudentId);
|
||||
const name = student ? student.identifier.replace(/\s+/g, '_') : 'report';
|
||||
const filename = `${name}_progress_report_${this.fromDate}_to_${this.toDate}.md`;
|
||||
|
||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||
// Prepend the prompt if one exists.
|
||||
let output = content;
|
||||
if (this.promptText.trim()) {
|
||||
output = this.promptText.trim() + '\n\n---\n\n' + content;
|
||||
}
|
||||
|
||||
const blob = new Blob([output], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ReportPromptDto {
|
||||
reportPromptId: string;
|
||||
programId: string;
|
||||
prompt: string;
|
||||
reportname: string;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ApiResult } from '../classes/api-result';
|
||||
import { ResponseResult } from '../classes/auth.models';
|
||||
import { ReportPromptDto } from '../classes/report-prompt.dto';
|
||||
import { describeHttpError } from '../classes/http-errors';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ReportPromptService {
|
||||
|
||||
// ************************** Declarations *************************
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = environment.apiBaseUrl;
|
||||
|
||||
// ************************ Public Methods *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Returns the report prompt for the given reportname, scoped to
|
||||
// the authenticated user's program.
|
||||
// *****************************************************************
|
||||
async getByReportname(name: string): Promise<ApiResult<ReportPromptDto>> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.get<ResponseResult<ReportPromptDto>>(
|
||||
`${this.base}/api/ReportPrompt/by-name/${encodeURIComponent(name)}`
|
||||
)
|
||||
);
|
||||
return result.success && result.data
|
||||
? ApiResult.ok(result.data)
|
||||
: ApiResult.fail(result.message);
|
||||
} catch (error) {
|
||||
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Updates the prompt text for an existing report prompt.
|
||||
// *****************************************************************
|
||||
async updatePrompt(id: string, prompt: string): Promise<ApiResult> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.put<ResponseResult<void>>(
|
||||
`${this.base}/api/ReportPrompt/${id}`,
|
||||
{ prompt }
|
||||
)
|
||||
);
|
||||
return result.success
|
||||
? ApiResult.empty()
|
||||
: ApiResult.fail(result.message);
|
||||
} catch (error) {
|
||||
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user