From a7850098cd6a59a66a2f889aecd12413b481796a Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:09:58 -0500 Subject: [PATCH 01/48] this is for uploading and downloading certificate template --- .../certifications-add.component.html | 9 ++- .../certifications-add.component.scss | 0 .../certifications-add.component.ts | 14 +++- .../certifications/certifications.service.ts | 13 ++++ src/app/shared/couchdb.service.ts | 73 ++++++++++--------- 5 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 src/app/manager-dashboard/certifications/certifications-add.component.scss diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 103cc379a5..b8f11ac2c7 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,13 +15,16 @@ + + + +
+
-
- - + diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 60b8de23c5..269a18ce6b 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -25,6 +25,8 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: string[] = []; pageType = 'Add'; disableRemove = true; + previewUrl: any; + selectedFile: any; @ViewChild(CoursesComponent) courseTable: CoursesComponent; constructor( @@ -78,6 +80,15 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.router.navigate([ navigation ], { relativeTo: this.route }); } + onFileSelected(event: any) { + if (event.target.files && event.target.files[0]) { + this.selectedFile = event.target.files[0]; + const reader = new FileReader(); + reader.onload = e => this.previewUrl = reader.result; + reader.readAsDataURL(this.selectedFile); + } + } + submitCertificate(reroute: boolean) { if (!this.certificateForm.valid) { showFormErrors(this.certificateForm.controls); @@ -87,7 +98,8 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificationsService.addCertification({ ...this.certificateInfo, ...certificateFormValue, - courseIds: this.courseIds + courseIds: this.courseIds, + attachment: this.selectedFile }).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index eb0e0a7eb3..9bd5fb3c27 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -4,6 +4,7 @@ import { PlanetMessageService } from '../../shared/planet-message.service'; import { MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; import { DialogsPromptComponent } from '../../shared/dialogs/dialogs-prompt.component'; import { dedupeShelfReduce } from '../../shared/utils'; +import { switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -52,6 +53,18 @@ export class CertificationsService { } addCertification(certification) { + const { attachment, ...cert } = certification; + if (attachment) { + return this.couchService.updateDocument(this.dbName, cert).pipe( + switchMap((res: any) => { + return this.couchService.putAttachment( + `${this.dbName}/${res.id}/attachment`, + attachment, + { rev: res.rev } + ); + }) + ); + } return this.couchService.updateDocument(this.dbName, { ...certification }); } diff --git a/src/app/shared/couchdb.service.ts b/src/app/shared/couchdb.service.ts index 7a41bd9bfa..ec0f08e559 100644 --- a/src/app/shared/couchdb.service.ts +++ b/src/app/shared/couchdb.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient, HttpRequest } from '@angular/common/http'; import { environment } from '../../environments/environment'; -import { Observable, of, empty, throwError, forkJoin } from 'rxjs'; -import { catchError, map, expand, toArray, flatMap, switchMap } from 'rxjs/operators'; +import { Observable, of, empty, throwError, forkJoin } from 'rxjs'; +import { catchError, map, expand, toArray, flatMap, switchMap } from 'rxjs/operators'; import { debug } from '../debug-operator'; import { PlanetMessageService } from './planet-message.service'; import { findDocuments } from './mangoQueries'; @@ -28,7 +28,8 @@ export class CouchService { const url = (domain ? (protocol || environment.parentProtocol) + '://' + domain : this.baseUrl) + '/' + db; let httpReq: Observable; if (type === 'post' || type === 'put') { - httpReq = this.http[type](url, data, opts); + const body = typeof data === 'object' && !(data instanceof Blob) ? JSON.stringify(data) : data; + httpReq = this.http[type](url, body, opts); } else { httpReq = this.http[type](url, opts); } @@ -68,10 +69,16 @@ export class CouchService { return this.couchDBReq('delete', db, this.setOpts(opts)); } - putAttachment(db: string, file: FormData, opts?: any) { - return this.couchDBReq('put', db, this.setOpts(opts), file); + putAttachment(db: string, file: File, opts?: any) { + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': file.type, + }), + withCredentials: true, + ...opts, + }; + return this.couchDBReq('put', db, this.setOpts(httpOptions), file); } - updateDocument(db: string, doc: any, opts?: any) { let docWithDate: any; return this.currentTime().pipe( @@ -97,33 +104,33 @@ export class CouchService { })); } - findAll(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { - return this.findAllRequest(db, query, opts).pipe(flatMap(({ docs }) => docs), toArray()); - } - - findAllStream(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { - return this.findAllRequest(db, query, opts).pipe(map(({ docs }) => docs)); - } - - findAttachmentsByIds(ids: string[], opts?: any) { - const uniqueIds = Array.from(new Set((ids || []).filter(id => !!id))); - if (uniqueIds.length === 0) { - return of([]); - } - const chunkSize = 50; - const queries = []; - for (let i = 0; i < uniqueIds.length; i += chunkSize) { - const chunk = uniqueIds.slice(i, i + chunkSize); - queries.push( - this.findAll('attachments', findDocuments({ '_id': { '$in': chunk } }, 0, 0, chunk.length), opts) - ); - } - return forkJoin(queries).pipe( - map((results: any[]) => results.reduce((acc: any[], docs: any[]) => acc.concat(docs), [])) - ); - } - - private findAllRequest(db: string, query: any, opts: any) { + findAll(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { + return this.findAllRequest(db, query, opts).pipe(flatMap(({ docs }) => docs), toArray()); + } + + findAllStream(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { + return this.findAllRequest(db, query, opts).pipe(map(({ docs }) => docs)); + } + + findAttachmentsByIds(ids: string[], opts?: any) { + const uniqueIds = Array.from(new Set((ids || []).filter(id => !!id))); + if (uniqueIds.length === 0) { + return of([]); + } + const chunkSize = 50; + const queries = []; + for (let i = 0; i < uniqueIds.length; i += chunkSize) { + const chunk = uniqueIds.slice(i, i + chunkSize); + queries.push( + this.findAll('attachments', findDocuments({ '_id': { '$in': chunk } }, 0, 0, chunk.length), opts) + ); + } + return forkJoin(queries).pipe( + map((results: any[]) => results.reduce((acc: any[], docs: any[]) => acc.concat(docs), [])) + ); + } + + private findAllRequest(db: string, query: any, opts: any) { return this.post(db + '/_find', query, opts).pipe( catchError(() => { return of({ docs: [], rows: [] }); From 09e48f0c5245df7545d140a1551c409e715a9c5e Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:17:16 -0500 Subject: [PATCH 02/48] commit to preview uploaded template --- .../certifications-add.component.html | 8 +-- .../certifications-add.component.ts | 19 ++++--- .../image-preview-dialog.component.html | 17 +++++++ .../dialogs/image-preview-dialog.component.ts | 50 +++++++++++++++++++ .../shared/dialogs/planet-dialogs.module.ts | 7 ++- 5 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 src/app/shared/dialogs/image-preview-dialog.component.html create mode 100644 src/app/shared/dialogs/image-preview-dialog.component.ts diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index b8f11ac2c7..1b3ff239d7 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,11 +15,7 @@ - - - -
- +
@@ -27,4 +23,4 @@ - +
diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 269a18ce6b..b08dc0ba9b 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -9,6 +9,7 @@ import { CoursesComponent } from '../../courses/courses.component'; import { showFormErrors } from '../../shared/table-helpers'; import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; +import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; interface CertificationFormModel { name: string; @@ -25,7 +26,6 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: string[] = []; pageType = 'Add'; disableRemove = true; - previewUrl: any; selectedFile: any; @ViewChild(CoursesComponent) courseTable: CoursesComponent; @@ -80,13 +80,16 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.router.navigate([ navigation ], { relativeTo: this.route }); } - onFileSelected(event: any) { - if (event.target.files && event.target.files[0]) { - this.selectedFile = event.target.files[0]; - const reader = new FileReader(); - reader.onload = e => this.previewUrl = reader.result; - reader.readAsDataURL(this.selectedFile); - } + openImagePreviewDialog() { + const dialogRef = this.dialog.open(ImagePreviewDialogComponent, { + data: { file: this.selectedFile } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result !== undefined) { + this.selectedFile = result; + } + }); } submitCertificate(reroute: boolean) { diff --git a/src/app/shared/dialogs/image-preview-dialog.component.html b/src/app/shared/dialogs/image-preview-dialog.component.html new file mode 100644 index 0000000000..e0a0562a29 --- /dev/null +++ b/src/app/shared/dialogs/image-preview-dialog.component.html @@ -0,0 +1,17 @@ +

Image Preview

+ +
+ +
+ +

No image selected.

+
+
+ + + + + + + + diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts new file mode 100644 index 0000000000..52cb90ae93 --- /dev/null +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -0,0 +1,50 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; + +@Component({ + templateUrl: './image-preview-dialog.component.html', +}) +export class ImagePreviewDialogComponent implements OnInit { + + previewUrl: any; + selectedFile: File; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) {} + + ngOnInit() { + if (this.data && this.data.file) { + this.selectedFile = this.data.file; + this.updatePreview(); + } + } + + onFileSelected(event: any) { + if (event.target.files && event.target.files[0]) { + this.selectedFile = event.target.files[0]; + this.updatePreview(); + } + } + + updatePreview() { + const reader = new FileReader(); + reader.onload = () => { + this.previewUrl = reader.result; + }; + reader.readAsDataURL(this.selectedFile); + } + + confirm() { + this.dialogRef.close(this.selectedFile); + } + + remove() { + this.dialogRef.close(null); + } + + close() { + this.dialogRef.close(); + } +} diff --git a/src/app/shared/dialogs/planet-dialogs.module.ts b/src/app/shared/dialogs/planet-dialogs.module.ts index 89162811f4..550f63cc06 100644 --- a/src/app/shared/dialogs/planet-dialogs.module.ts +++ b/src/app/shared/dialogs/planet-dialogs.module.ts @@ -17,6 +17,7 @@ import { SyncDirective } from '../../manager-dashboard/sync.directive'; import { DialogsImagesComponent } from './dialogs-images.component'; import { DialogsAnnouncementComponent, DialogsAnnouncementSuccessComponent } from './dialogs-announcement.component'; import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-ratings.component'; +import { ImagePreviewDialogComponent } from './image-preview-dialog.component'; @NgModule({ imports: [ @@ -38,7 +39,8 @@ import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-rati DialogsRatingsComponent, DialogsRatingsDirective, ChangePasswordDirective, - SyncDirective + SyncDirective, + ImagePreviewDialogComponent ], declarations: [ DialogsFormComponent, @@ -53,7 +55,8 @@ import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-rati ChangePasswordDirective, SyncDirective, DialogsAnnouncementComponent, - DialogsAnnouncementSuccessComponent + DialogsAnnouncementSuccessComponent, + ImagePreviewDialogComponent ], providers: [ DialogsFormService, From 903c74ede9162d0213ddba91e198a5aff9ff7129 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:18:40 -0500 Subject: [PATCH 03/48] commit --- .../certifications/certifications-add.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1b3ff239d7..1d16415db4 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,7 +15,7 @@ - +
From 76cc154f15787c5656d4af94bcad2bf926cedc1f Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:25:14 -0500 Subject: [PATCH 04/48] enhanced css for styling. --- .../image-preview-dialog.component.scss | 73 +++++++++++++++++++ .../dialogs/image-preview-dialog.component.ts | 1 + 2 files changed, 74 insertions(+) create mode 100644 src/app/shared/dialogs/image-preview-dialog.component.scss diff --git a/src/app/shared/dialogs/image-preview-dialog.component.scss b/src/app/shared/dialogs/image-preview-dialog.component.scss new file mode 100644 index 0000000000..01094880fb --- /dev/null +++ b/src/app/shared/dialogs/image-preview-dialog.component.scss @@ -0,0 +1,73 @@ +/* Dialog content layout */ +.mat-dialog-content { + display: flex; + justify-content: center; + align-items: center; + padding: 24px; + max-height: 70vh; + overflow: auto; +} + +/* Dialog action buttons */ +.mat-dialog-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; + padding: 16px 24px 24px; + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +/* Image preview styling */ +img { + max-width: 100%; + max-height: 60vh; + height: auto; + border-radius: 8px; + border: 1px solid #e0e0e0; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +/* Subtle hover effect for images */ +img:hover { + transform: scale(1.01); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); +} + +/* Material buttons spacing */ +.mat-button, +.mat-raised-button { + min-width: 96px; +} + +/* Primary raised button enhancement */ +.mat-raised-button.mat-primary { + background-color: #3f51b5; /* Material Indigo */ + color: #fff; + font-weight: 500; + box-shadow: 0 3px 6px rgba(63, 81, 181, 0.3); + transition: background-color 0.2s ease, box-shadow 0.2s ease; +} + +.mat-raised-button.mat-primary:hover { + background-color: #35449a; + box-shadow: 0 6px 12px rgba(63, 81, 181, 0.4); +} + +/* Mobile responsiveness */ +@media (max-width: 600px) { + .mat-dialog-content { + padding: 16px; + } + + .mat-dialog-actions { + padding: 12px 16px 16px; + flex-direction: column; + align-items: stretch; + } + + .mat-dialog-actions button { + width: 100%; + } +} diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index 52cb90ae93..25bd39b7ab 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -3,6 +3,7 @@ import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALO @Component({ templateUrl: './image-preview-dialog.component.html', + styleUrls: ['./image-preview-dialog.component.scss'] }) export class ImagePreviewDialogComponent implements OnInit { From 5c82cfa540411c94f5e54ba52f7ed91afe4ad804 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:27:48 -0500 Subject: [PATCH 05/48] enhancement to remove button --- src/app/shared/dialogs/image-preview-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index 25bd39b7ab..ac4484acda 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -42,7 +42,8 @@ export class ImagePreviewDialogComponent implements OnInit { } remove() { - this.dialogRef.close(null); + this.selectedFile = null; + this.previewUrl = null; } close() { From ba0e85c92abc562e5a20d5a7a432e69007af4745 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:30:12 -0500 Subject: [PATCH 06/48] commit --- src/app/shared/dialogs/image-preview-dialog.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index ac4484acda..13c0fd9998 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnInit, ViewChild, ElementRef } from '@angular/core'; import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; @Component({ @@ -9,6 +9,7 @@ export class ImagePreviewDialogComponent implements OnInit { previewUrl: any; selectedFile: File; + @ViewChild('fileInput') fileInput: ElementRef; constructor( public dialogRef: MatDialogRef, @@ -26,6 +27,7 @@ export class ImagePreviewDialogComponent implements OnInit { if (event.target.files && event.target.files[0]) { this.selectedFile = event.target.files[0]; this.updatePreview(); + this.fileInput.nativeElement.value = ''; } } From 183c081c1a98d1596dcd9e9fbcdb924f5ab63fd1 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:34:53 -0500 Subject: [PATCH 07/48] commit --- .../certifications-add.component.ts | 36 ++++++++++++++++++- .../image-preview-dialog.component.html | 2 +- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index b08dc0ba9b..84f75061ac 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -59,10 +59,12 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; + this.loadAttachment(); }); } else { - this.certificateInfo._id = undefined; + this.certificateInfo._id = `temp-${Date.now()}`; this.courseIds = []; + this.loadAttachment(); } }); } @@ -88,10 +90,41 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { this.selectedFile = result; + this.saveAttachment(); } }); } + loadAttachment() { + const storedAttachment = localStorage.getItem(`certification-attachment-${this.certificateInfo._id}`); + if (storedAttachment) { + const attachment = JSON.parse(storedAttachment); + const byteCharacters = atob(attachment.data.split(',')[1]); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + this.selectedFile = new File([byteArray], attachment.name, { type: attachment.type }); + } + } + + saveAttachment() { + if (this.selectedFile) { + const reader = new FileReader(); + reader.onload = () => { + localStorage.setItem(`certification-attachment-${this.certificateInfo._id}`, JSON.stringify({ + name: this.selectedFile.name, + type: this.selectedFile.type, + data: reader.result + })); + }; + reader.readAsDataURL(this.selectedFile); + } else { + localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); + } + } + submitCertificate(reroute: boolean) { if (!this.certificateForm.valid) { showFormErrors(this.certificateForm.controls); @@ -104,6 +137,7 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: this.courseIds, attachment: this.selectedFile }).subscribe((res) => { + localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` diff --git a/src/app/shared/dialogs/image-preview-dialog.component.html b/src/app/shared/dialogs/image-preview-dialog.component.html index e0a0562a29..38260000ac 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.html +++ b/src/app/shared/dialogs/image-preview-dialog.component.html @@ -9,7 +9,7 @@

Image Preview

- + From 0624c79236b4aebf6124340de2bf40287b5124b4 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:30:31 -0500 Subject: [PATCH 08/48] commit --- .../certifications/certifications-add.component.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1d16415db4..a33fbda5ec 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,12 +15,14 @@ - + + - + + \ No newline at end of file From 786f0ac7896e32fdfcd55601d83f0d45ae1a5598 Mon Sep 17 00:00:00 2001 From: Emmanuel Baah Date: Thu, 18 Dec 2025 14:31:54 -0500 Subject: [PATCH 09/48] Update certifications-add.component.html --- .../certifications/certifications-add.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index a33fbda5ec..be0a8fe644 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -25,4 +25,4 @@ - \ No newline at end of file + From 60fd86b1b8bf649959356c2eb2995d09b6ab7294 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:34:02 -0500 Subject: [PATCH 10/48] commit --- .../certifications/certifications-add.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index be0a8fe644..1a0ed96df4 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,7 +15,7 @@ - +
From 24ef588e1c0aa10bb059675e3d26f7880d18df3a Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:48:50 -0500 Subject: [PATCH 11/48] feat: Implement certificate background image from template Add getAttachment method to CouchService to enable fetching of attachment data from CouchDB. Modify UsersAchievementsComponent to fetch certificate template attachments and use them as background images in the generated PDF achievements. This addresses the feedback regarding using doc.addImage for efficiency. --- src/app/shared/couchdb.service.ts | 10 +++ .../users-achievements.component.ts | 63 +++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/app/shared/couchdb.service.ts b/src/app/shared/couchdb.service.ts index ec0f08e559..24564f72c2 100644 --- a/src/app/shared/couchdb.service.ts +++ b/src/app/shared/couchdb.service.ts @@ -232,4 +232,14 @@ export class CouchService { ); } + getAttachment(db: string, docId: string, attachmentId: string, opts?: any): Observable { + const url = `${this.baseUrl}/${db}/${docId}/${attachmentId}`; + const httpOptions = { + ...this.defaultOpts, + responseType: 'blob' as 'json', + ...opts, + }; + return this.couchDBReq('get', url, this.setOpts(httpOptions)); + } + } diff --git a/src/app/users/users-achievements/users-achievements.component.ts b/src/app/users/users-achievements/users-achievements.component.ts index 941cfec493..ee02ae0f26 100644 --- a/src/app/users/users-achievements/users-achievements.component.ts +++ b/src/app/users/users-achievements/users-achievements.component.ts @@ -5,8 +5,8 @@ import { CouchService } from '../../shared/couchdb.service'; import { UserService } from '../../shared/user.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { UsersAchievementsService } from './users-achievements.service'; -import { catchError, auditTime } from 'rxjs/operators'; -import { throwError, combineLatest } from 'rxjs'; +import { catchError, auditTime, switchMap, map } from 'rxjs/operators'; // Added switchMap and map +import { throwError, combineLatest, forkJoin, Observable, of } from 'rxjs'; // Added forkJoin, Observable, of import { StateService } from '../../shared/state.service'; import { CoursesService } from '../../courses/courses.service'; import { environment } from '../../../environments/environment'; @@ -124,12 +124,52 @@ export class UsersAchievementsComponent implements OnInit { } setCertifications(courses = [], progress = [], certifications = []) { - this.certifications = certifications.filter(certification => { + const filteredCertifications = certifications.filter(certification => { const certificateCourses = courses .filter(course => certification.courseIds.indexOf(course._id) > -1) .map(course => ({ ...course, progress: progress.filter(p => p.courseId === course._id) })); return certificateCourses.every(course => this.certificationsService.isCourseCompleted(course, this.user)); }); + + const attachmentFetches: Observable[] = filteredCertifications.map(certification => { + // Check if _attachments exists and has an attachment named 'attachment' + if (certification._attachments && certification._attachments.attachment) { + return this.couchService.getAttachment(this.certificationsService.dbName, certification._id, 'attachment').pipe( + switchMap(blob => { + return new Observable(observer => { + const reader = new FileReader(); + reader.onloadend = () => { + // Ensure reader.result is a string (base64) before assigning + certification.templateImage = typeof reader.result === 'string' ? reader.result : null; + observer.next(certification); + observer.complete(); + }; + reader.onerror = (error) => { + console.error('Error reading blob as DataURL', error); + certification.templateImage = null; // Set to null on error + observer.next(certification); + observer.complete(); + }; + reader.readAsDataURL(blob); + }); + }), + catchError(err => { + console.error('Error fetching attachment for certification', certification._id, err); + return of(certification); // Return certification without attachment if error + }) + ); + } else { + return of(certification); // No attachment, return certification as is + } + }); + + if (attachmentFetches.length > 0) { + forkJoin(attachmentFetches).subscribe(certsWithAttachments => { + this.certifications = certsWithAttachments; + }); + } else { + this.certifications = filteredCertifications; + } } copyLink() { @@ -232,7 +272,7 @@ export class UsersAchievementsComponent implements OnInit { contentArray = contentArray.concat(optionals); - const documentDefinition = { + let documentDefinition: any = { // Changed to let to allow modification content: contentArray, styles: { header: { @@ -246,6 +286,21 @@ export class UsersAchievementsComponent implements OnInit { }, }; + // Find a certification with a template image + const certificationWithTemplate = this.certifications.find(cert => cert.templateImage); + + if (certificationWithTemplate && certificationWithTemplate.templateImage) { + documentDefinition.background = [ + { + image: certificationWithTemplate.templateImage, + width: 612, // US Letter width in points (8.5 inches * 72 points/inch) + height: 792, // US Letter height in points (11 inches * 72 points/inch) + absolutePosition: { x: 0, y: 0 }, + opacity: 0.5 // Optional: adjust opacity for readability + } + ]; + } + pdfMake .createPdf(documentDefinition) .download($localize`${this.user.name} achievements.pdf`); From fdcc297b21d01763fa5e85f1854c3a1640faa42a Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:53:17 -0500 Subject: [PATCH 12/48] refactor: Remove localStorage for certificate attachments Refactor CertificationsAddComponent to remove the usage of localStorage for temporarily storing certificate attachments. The component now fetches attachments directly from CouchDB when editing an existing certification. The selected file is held in memory until the form is submitted. --- .../certifications-add.component.ts | 40 +++---------------- .../certifications/certifications.service.ts | 4 ++ 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 84f75061ac..ac9d64ed19 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -59,12 +59,16 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; - this.loadAttachment(); + // Load attachment from CouchDB if it exists + if (certification._attachments && certification._attachments.attachment) { + this.certificationsService.getAttachment(id, 'attachment').subscribe(blob => { + this.selectedFile = new File([blob], 'attachment'); // Create a File object + }); + } }); } else { this.certificateInfo._id = `temp-${Date.now()}`; this.courseIds = []; - this.loadAttachment(); } }); } @@ -90,41 +94,10 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { this.selectedFile = result; - this.saveAttachment(); } }); } - loadAttachment() { - const storedAttachment = localStorage.getItem(`certification-attachment-${this.certificateInfo._id}`); - if (storedAttachment) { - const attachment = JSON.parse(storedAttachment); - const byteCharacters = atob(attachment.data.split(',')[1]); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - this.selectedFile = new File([byteArray], attachment.name, { type: attachment.type }); - } - } - - saveAttachment() { - if (this.selectedFile) { - const reader = new FileReader(); - reader.onload = () => { - localStorage.setItem(`certification-attachment-${this.certificateInfo._id}`, JSON.stringify({ - name: this.selectedFile.name, - type: this.selectedFile.type, - data: reader.result - })); - }; - reader.readAsDataURL(this.selectedFile); - } else { - localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); - } - } - submitCertificate(reroute: boolean) { if (!this.certificateForm.valid) { showFormErrors(this.certificateForm.controls); @@ -137,7 +110,6 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: this.courseIds, attachment: this.selectedFile }).subscribe((res) => { - localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index 9bd5fb3c27..800f9364bd 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -28,6 +28,10 @@ export class CertificationsService { return this.couchService.get(`${this.dbName}/${id}`); } + getAttachment(docId: string, attachmentId: string) { + return this.couchService.getAttachment(this.dbName, docId, attachmentId); + } + openDeleteDialog(certification: any, callback) { const displayName = certification.name; this.deleteDialog = this.dialog.open(DialogsPromptComponent, { From 6aa19abccfe86f13a927a103246c41e9d8fd863a Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:00:01 -0500 Subject: [PATCH 13/48] feat: Persist certificate attachments in CouchDB on selection Implement a 'draft' workflow for certificate attachments. When a user selects a file for a new certificate, a draft document is created in CouchDB and the file is uploaded as an attachment. This ensures that the selected attachment persists across page reloads and navigations, using CouchDB as the storage backend instead of localStorage. --- .../certifications-add.component.ts | 39 ++++++++++++++++--- .../certifications/certifications.service.ts | 12 ++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index ac9d64ed19..5e887d3569 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -10,6 +10,7 @@ import { showFormErrors } from '../../shared/table-helpers'; import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; +import { switchMap } from 'rxjs/operators'; interface CertificationFormModel { name: string; @@ -66,9 +67,6 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { }); } }); - } else { - this.certificateInfo._id = `temp-${Date.now()}`; - this.courseIds = []; } }); } @@ -94,6 +92,33 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { this.selectedFile = result; + // If it's a new certification (no _id from route), create a draft. + if (!this.route.snapshot.paramMap.get('id')) { + // If no doc exists yet, create one + if (!this.certificateInfo._id || this.certificateInfo._id.startsWith('temp-')) { + this.certificationsService.createDraftCertification().pipe( + switchMap((res: any) => { + this.certificateInfo = { _id: res.id, _rev: res.rev }; + return this.certificationsService.uploadAttachment(res.id, res.rev, this.selectedFile); + }) + ).subscribe(uploadRes => { + this.certificateInfo._rev = uploadRes.rev; // Update rev after attachment upload + this.planetMessageService.showMessage($localize`Draft saved.`); + }); + } else { // Draft doc already exists, just upload attachment + this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) + .subscribe(uploadRes => { + this.certificateInfo._rev = uploadRes.rev; + this.planetMessageService.showMessage($localize`Draft updated.`); + }); + } + } else { // Existing certification, just upload the new attachment + this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) + .subscribe(uploadRes => { + this.certificateInfo._rev = uploadRes.rev; + this.planetMessageService.showMessage($localize`Attachment updated.`); + }); + } } }); } @@ -104,12 +129,14 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { return; } const certificateFormValue: CertificationFormModel = this.certificateForm.getRawValue(); - this.certificationsService.addCertification({ + const certData = { ...this.certificateInfo, ...certificateFormValue, courseIds: this.courseIds, - attachment: this.selectedFile - }).subscribe((res) => { + type: 'certification' // Ensure type is 'certification', not 'draft' + }; + // The attachment is already in CouchDB, so we just update the document fields. + this.certificationsService.addCertification(certData).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index 800f9364bd..9ec4aeebaf 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -72,6 +72,18 @@ export class CertificationsService { return this.couchService.updateDocument(this.dbName, { ...certification }); } + createDraftCertification() { + return this.couchService.updateDocument(this.dbName, { type: 'draft' }); + } + + uploadAttachment(docId: string, rev: string, file: File) { + return this.couchService.putAttachment( + `${this.dbName}/${docId}/attachment`, + file, + { rev } + ); + } + isCourseCompleted(course, user) { return course.doc.steps.length === course.progress .filter(step => step.userId === user._id && step.passed) From 3858c47ac407361627f8a0b020889193ae8ae501 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:20:05 -0500 Subject: [PATCH 14/48] fix(attachments): Correct getAttachment method in CouchService The getAttachment method was using couchDBReq with a full URL, which was incorrect. It was also casting the responseType of 'blob' as 'json'. This commit corrects the getAttachment method to use http.get directly with the correct responseType ('blob') and proper URL construction and error handling. This should resolve the issue of attachments not being loaded correctly on page refresh. --- src/app/shared/couchdb.service.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/shared/couchdb.service.ts b/src/app/shared/couchdb.service.ts index 24564f72c2..ee6b9c08b9 100644 --- a/src/app/shared/couchdb.service.ts +++ b/src/app/shared/couchdb.service.ts @@ -236,10 +236,20 @@ export class CouchService { const url = `${this.baseUrl}/${db}/${docId}/${attachmentId}`; const httpOptions = { ...this.defaultOpts, - responseType: 'blob' as 'json', + responseType: 'blob', ...opts, }; - return this.couchDBReq('get', url, this.setOpts(httpOptions)); + // Use http.get directly instead of couchDBReq because couchDBReq is not suitable for attachment URLs + return this.http.get(url, httpOptions).pipe( + catchError(err => { + if (err.status === 403) { + this.planetMessageService.showAlert($localize`You are not authorized. Please contact administrator.`); + } else { + this.planetMessageService.showAlert($localize`Error fetching attachment: ${err.message}`); + } + return throwError(err); + }) + ); } } From fb4dfc6789d52e361ca08b76ddab522085b5e407 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:36:35 -0500 Subject: [PATCH 15/48] refactor(certifications): Align attachment handling with edit profile Refactor certificate attachment handling to follow the same pattern as the user profile edit page. - Remove the 'draft' workflow and associated service methods. - Load existing attachments using a direct URL for previews. - Convert newly selected images to base64 and send them inline with the document on submission. This ensures that attachments are persisted correctly and are not lost on page refresh, addressing the user's feedback. --- .../certifications-add.component.ts | 72 +++++++++---------- .../certifications/certifications.service.ts | 31 +------- 2 files changed, 36 insertions(+), 67 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 5e887d3569..7cb2af7de5 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -10,7 +10,7 @@ import { showFormErrors } from '../../shared/table-helpers'; import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; -import { switchMap } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; interface CertificationFormModel { name: string; @@ -27,7 +27,10 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: string[] = []; pageType = 'Add'; disableRemove = true; - selectedFile: any; + selectedFile: any; // Will hold base64 string for new files + previewSrc: any = 'assets/image.png'; // For image preview + urlPrefix = environment.couchAddress + '/' + this.dbName + '/'; + @ViewChild(CoursesComponent) courseTable: CoursesComponent; constructor( @@ -60,11 +63,9 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; - // Load attachment from CouchDB if it exists if (certification._attachments && certification._attachments.attachment) { - this.certificationsService.getAttachment(id, 'attachment').subscribe(blob => { - this.selectedFile = new File([blob], 'attachment'); // Create a File object - }); + // Construct direct URL for the preview + this.previewSrc = `${this.urlPrefix}${id}/attachment`; } }); } @@ -86,38 +87,23 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { openImagePreviewDialog() { const dialogRef = this.dialog.open(ImagePreviewDialogComponent, { - data: { file: this.selectedFile } + data: { file: this.previewSrc } // Pass previewSrc to the dialog }); dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { - this.selectedFile = result; - // If it's a new certification (no _id from route), create a draft. - if (!this.route.snapshot.paramMap.get('id')) { - // If no doc exists yet, create one - if (!this.certificateInfo._id || this.certificateInfo._id.startsWith('temp-')) { - this.certificationsService.createDraftCertification().pipe( - switchMap((res: any) => { - this.certificateInfo = { _id: res.id, _rev: res.rev }; - return this.certificationsService.uploadAttachment(res.id, res.rev, this.selectedFile); - }) - ).subscribe(uploadRes => { - this.certificateInfo._rev = uploadRes.rev; // Update rev after attachment upload - this.planetMessageService.showMessage($localize`Draft saved.`); - }); - } else { // Draft doc already exists, just upload attachment - this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) - .subscribe(uploadRes => { - this.certificateInfo._rev = uploadRes.rev; - this.planetMessageService.showMessage($localize`Draft updated.`); - }); - } - } else { // Existing certification, just upload the new attachment - this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) - .subscribe(uploadRes => { - this.certificateInfo._rev = uploadRes.rev; - this.planetMessageService.showMessage($localize`Attachment updated.`); - }); + if (result instanceof File) { + // New file selected, convert to base64 + const reader = new FileReader(); + reader.onload = () => { + this.selectedFile = reader.result; // base64 string + this.previewSrc = reader.result; + }; + reader.readAsDataURL(result); + } else if (result === null) { + // Image removed + this.selectedFile = null; + this.previewSrc = 'assets/image.png'; } } }); @@ -129,13 +115,25 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { return; } const certificateFormValue: CertificationFormModel = this.certificateForm.getRawValue(); - const certData = { + const certData: any = { ...this.certificateInfo, ...certificateFormValue, courseIds: this.courseIds, - type: 'certification' // Ensure type is 'certification', not 'draft' }; - // The attachment is already in CouchDB, so we just update the document fields. + + if (this.selectedFile && this.selectedFile.startsWith('data:')) { + // New base64 image data is present + const imgDataArr: string[] = this.selectedFile.split(/;\w+,/); + const contentType: string = imgDataArr[0].replace(/data:/, ''); + const data: string = imgDataArr[1]; + certData._attachments = { + 'attachment': { + 'content_type': contentType, + 'data': data + } + }; + } + this.certificationsService.addCertification(certData).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index 9ec4aeebaf..e2b4fe9f93 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -4,7 +4,6 @@ import { PlanetMessageService } from '../../shared/planet-message.service'; import { MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; import { DialogsPromptComponent } from '../../shared/dialogs/dialogs-prompt.component'; import { dedupeShelfReduce } from '../../shared/utils'; -import { switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -28,10 +27,6 @@ export class CertificationsService { return this.couchService.get(`${this.dbName}/${id}`); } - getAttachment(docId: string, attachmentId: string) { - return this.couchService.getAttachment(this.dbName, docId, attachmentId); - } - openDeleteDialog(certification: any, callback) { const displayName = certification.name; this.deleteDialog = this.dialog.open(DialogsPromptComponent, { @@ -57,31 +52,7 @@ export class CertificationsService { } addCertification(certification) { - const { attachment, ...cert } = certification; - if (attachment) { - return this.couchService.updateDocument(this.dbName, cert).pipe( - switchMap((res: any) => { - return this.couchService.putAttachment( - `${this.dbName}/${res.id}/attachment`, - attachment, - { rev: res.rev } - ); - }) - ); - } - return this.couchService.updateDocument(this.dbName, { ...certification }); - } - - createDraftCertification() { - return this.couchService.updateDocument(this.dbName, { type: 'draft' }); - } - - uploadAttachment(docId: string, rev: string, file: File) { - return this.couchService.putAttachment( - `${this.dbName}/${docId}/attachment`, - file, - { rev } - ); + return this.couchService.updateDocument(this.dbName, certification); } isCourseCompleted(course, user) { From 317aec4919d5beaa2e88a14606dbc270a8212c16 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:42:50 -0500 Subject: [PATCH 16/48] fix(certifications): Add image preview to certifications add/edit page Add an image preview to the template to display the certificate template. The preview is only shown if it's not the default placeholder image. Also, change the 'Upload' button text to 'Change Template' if an image is already present. This provides visual feedback to the user and resolves the issue of the attachment appearing to be lost on page refresh. --- .../certifications/certifications-add.component.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1a0ed96df4..930552c7f0 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,12 +10,19 @@ {pageType, select, Add {Add} Update {Update}} Certification
+
+

Certificate Template Preview:

+ Certificate Template Preview +
- +
From 5915d97b736c1e57a9250fa0a85d2cc93af83204 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:47:05 -0500 Subject: [PATCH 17/48] revert: Remove on-page preview for certificate template Revert the change that added an on-page preview for the certificate template. The user has requested that the preview only be visible in the dialog. The underlying logic for attachment persistence remains in place. --- .../certifications/certifications-add.component.html | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 930552c7f0..1a0ed96df4 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,19 +10,12 @@ {pageType, select, Add {Add} Update {Update}} Certification
-
-

Certificate Template Preview:

- Certificate Template Preview -
- +
From 0bae2068cd130e6541c86337ba569fc879ae5f68 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:53:45 -0500 Subject: [PATCH 18/48] fix(certifications): Restore image preview to fix persistence issue Re-adds the image preview to the certificate add/edit page. This resolves the issue where the attachment appeared to be lost on page refresh because it was not being displayed. The component now shows a preview of the existing template on load, and updates the preview when a new template is selected. --- .../certifications/certifications-add.component.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1a0ed96df4..930552c7f0 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,12 +10,19 @@ {pageType, select, Add {Add} Update {Update}} Certification
+
+

Certificate Template Preview:

+ Certificate Template Preview +
- +
From 85d792fced87fa96bb6f94b3e65847f79b4415f0 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:01:01 -0500 Subject: [PATCH 19/48] commit --- .../certifications-add.component.scss | 115 ++++++++++++++++++ .../certifications-add.component.ts | 3 +- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index e69de29bb2..c26d6a3442 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -0,0 +1,115 @@ +/* ===== Toolbar ===== */ +mat-toolbar { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} + +.btnBack { + margin-right: 8px; +} + +/* ===== Page Container ===== */ +.space-container { + padding: 24px; + max-width: 1100px; + margin: 0 auto; +} + +/* ===== Section Header ===== */ +.space-container > mat-toolbar { + border-radius: 10px; + margin-bottom: 24px; + padding: 12px 20px; + font-size: 1.1rem; +} + +/* ===== Main Content Card ===== */ +.view-container { + background: #ffffff; + border-radius: 16px; + padding: 28px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +/* ===== Certificate Preview ===== */ +.attachment-preview { + margin-bottom: 24px; + padding: 16px; + border-radius: 12px; + background: #f9fafb; + border: 1px dashed #d1d5db; + text-align: center; +} + +.attachment-preview p { + margin-bottom: 12px; + font-weight: 500; + color: #374151; +} + +.attachment-preview img { + max-width: 220px; + border-radius: 8px; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.attachment-preview img:hover { + transform: scale(1.03); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15); +} + +/* ===== Action Buttons ===== */ +.action-buttons { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 28px; +} + +.action-buttons button { + border-radius: 24px; + padding: 8px 20px; + font-weight: 500; +} + +/* Primary emphasis */ +.action-buttons button[color="primary"] { + box-shadow: 0 4px 12px rgba(25, 118, 210, 0.25); +} + +/* ===== Form ===== */ +.full-width { + width: 100%; + margin-bottom: 24px; +} + +mat-form-field { + font-size: 1rem; +} + +/* ===== Courses Section ===== */ +planet-courses { + display: block; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #e5e7eb; +} + +/* ===== Responsive Tweaks ===== */ +@media (max-width: 768px) { + .space-container { + padding: 16px; + } + + .view-container { + padding: 20px; + } + + .action-buttons { + flex-direction: column; + align-items: stretch; + } +} diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 7cb2af7de5..f6af960108 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -17,7 +17,8 @@ interface CertificationFormModel { } @Component({ - templateUrl: './certifications-add.component.html' + templateUrl: './certifications-add.component.html', + styleUrls: ['./certifications-add.component.scss'] }) export class CertificationsAddComponent implements OnInit, AfterViewChecked { From 3ca097b659c66e51dc2f11ed92750b6c3ba563d1 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:07:49 -0500 Subject: [PATCH 20/48] fix(certifications): Implement robust attachment handling Refactor the certificate add/edit component to correctly handle image attachments, preventing data loss on page refresh. - The component now displays a preview of the existing certificate template on load. - Implemented an 'unsaved changes' guard to prevent accidental navigation away from the page with unsaved modifications to the form or the attachment. - The image preview dialog is now more robust and can handle both File objects and URL strings. - Aligned the attachment handling pattern with the existing 'edit profile' functionality for consistency. --- .../certifications-add.component.scss | 115 ------------------ .../certifications-add.component.ts | 82 ++++++++++--- .../dialogs/image-preview-dialog.component.ts | 31 +++-- 3 files changed, 87 insertions(+), 141 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index c26d6a3442..e69de29bb2 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -1,115 +0,0 @@ -/* ===== Toolbar ===== */ -mat-toolbar { - display: flex; - align-items: center; - gap: 8px; - font-weight: 500; -} - -.btnBack { - margin-right: 8px; -} - -/* ===== Page Container ===== */ -.space-container { - padding: 24px; - max-width: 1100px; - margin: 0 auto; -} - -/* ===== Section Header ===== */ -.space-container > mat-toolbar { - border-radius: 10px; - margin-bottom: 24px; - padding: 12px 20px; - font-size: 1.1rem; -} - -/* ===== Main Content Card ===== */ -.view-container { - background: #ffffff; - border-radius: 16px; - padding: 28px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); -} - -/* ===== Certificate Preview ===== */ -.attachment-preview { - margin-bottom: 24px; - padding: 16px; - border-radius: 12px; - background: #f9fafb; - border: 1px dashed #d1d5db; - text-align: center; -} - -.attachment-preview p { - margin-bottom: 12px; - font-weight: 500; - color: #374151; -} - -.attachment-preview img { - max-width: 220px; - border-radius: 8px; - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.attachment-preview img:hover { - transform: scale(1.03); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15); -} - -/* ===== Action Buttons ===== */ -.action-buttons { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-bottom: 28px; -} - -.action-buttons button { - border-radius: 24px; - padding: 8px 20px; - font-weight: 500; -} - -/* Primary emphasis */ -.action-buttons button[color="primary"] { - box-shadow: 0 4px 12px rgba(25, 118, 210, 0.25); -} - -/* ===== Form ===== */ -.full-width { - width: 100%; - margin-bottom: 24px; -} - -mat-form-field { - font-size: 1rem; -} - -/* ===== Courses Section ===== */ -planet-courses { - display: block; - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid #e5e7eb; -} - -/* ===== Responsive Tweaks ===== */ -@media (max-width: 768px) { - .space-container { - padding: 16px; - } - - .view-container { - padding: 20px; - } - - .action-buttons { - flex-direction: column; - align-items: stretch; - } -} diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index f6af960108..b3c116921c 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild, AfterViewChecked, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, ViewChild, AfterViewChecked, ChangeDetectorRef, HostListener } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; @@ -11,6 +11,8 @@ import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; import { environment } from '../../../environments/environment'; +import { CanComponentDeactivate } from '../../shared/unsaved-changes.guard'; +import { warningMsg } from '../../shared/unsaved-changes.component'; interface CertificationFormModel { name: string; @@ -20,7 +22,7 @@ interface CertificationFormModel { templateUrl: './certifications-add.component.html', styleUrls: ['./certifications-add.component.scss'] }) -export class CertificationsAddComponent implements OnInit, AfterViewChecked { +export class CertificationsAddComponent implements OnInit, AfterViewChecked, CanComponentDeactivate { readonly dbName = 'certifications'; certificateInfo: { _id?: string, _rev?: string } = {}; @@ -32,6 +34,10 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { previewSrc: any = 'assets/image.png'; // For image preview urlPrefix = environment.couchAddress + '/' + this.dbName + '/'; + initialFormValues: any; + attachmentChanged = false; + isFormInitialized = false; + @ViewChild(CoursesComponent) courseTable: CoursesComponent; constructor( @@ -61,6 +67,7 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._id = id; this.certificationsService.getCertification(id).subscribe(certification => { this.certificateForm.patchValue({ name: certification.name || '' } as Partial); + this.initialFormValues = { ...this.certificateForm.value }; this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; @@ -68,11 +75,23 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { // Construct direct URL for the preview this.previewSrc = `${this.urlPrefix}${id}/attachment`; } + this.isFormInitialized = true; + this.setupFormValueChanges(); }); + } else { + this.initialFormValues = { ...this.certificateForm.value }; + this.isFormInitialized = true; + this.setupFormValueChanges(); } }); } + setupFormValueChanges() { + this.certificateForm.valueChanges.subscribe(() => { + // The guard will check for changes, no need to set a flag here + }); + } + ngAfterViewChecked() { const disableRemove = !this.courseTable || !this.courseTable.selection.selected.length; if (this.disableRemove !== disableRemove) { @@ -92,20 +111,22 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { }); dialogRef.afterClosed().subscribe(result => { - if (result !== undefined) { - if (result instanceof File) { - // New file selected, convert to base64 - const reader = new FileReader(); - reader.onload = () => { - this.selectedFile = reader.result; // base64 string - this.previewSrc = reader.result; - }; - reader.readAsDataURL(result); - } else if (result === null) { - // Image removed - this.selectedFile = null; - this.previewSrc = 'assets/image.png'; - } + if (result === undefined) { + return; // Dialog was closed without action + } + this.attachmentChanged = true; // Any confirmed action in the dialog is a change + if (result instanceof File) { + // New file selected, convert to base64 + const reader = new FileReader(); + reader.onload = () => { + this.selectedFile = reader.result as string; + this.previewSrc = this.selectedFile; + }; + reader.readAsDataURL(result); + } else if (result === null) { + // Image removed + this.selectedFile = null; + this.previewSrc = 'assets/image.png'; } }); } @@ -133,10 +154,18 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { 'data': data } }; + } else if (this.attachmentChanged && !this.selectedFile) { + // This means the image was removed + if (certData._attachments) { + delete certData._attachments; + } } + this.certificationsService.addCertification(certData).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; + this.initialFormValues = { ...this.certificateForm.value }; + this.attachmentChanged = false; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` ); @@ -166,4 +195,25 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.courseIds = this.courseIds.filter(id => this.courseTable.selection.selected.indexOf(id) === -1); } + canDeactivate(): boolean { + return !this.getHasUnsavedChanges(); + } + + isFormPristine(): boolean { + return JSON.stringify(this.certificateForm.value) === JSON.stringify(this.initialFormValues); + } + + @HostListener('window:beforeunload', [ '$event' ]) + unloadNotification($event: BeforeUnloadEvent): void { + if (this.getHasUnsavedChanges()) { + $event.returnValue = warningMsg; + } + } + + private getHasUnsavedChanges(): boolean { + if (!this.isFormInitialized) { + return false; + } + return !this.isFormPristine() || this.attachmentChanged; + } } diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index 13c0fd9998..c55cf97947 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -8,7 +8,7 @@ import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALO export class ImagePreviewDialogComponent implements OnInit { previewUrl: any; - selectedFile: File; + selectedFile: File | null; @ViewChild('fileInput') fileInput: ElementRef; constructor( @@ -18,8 +18,13 @@ export class ImagePreviewDialogComponent implements OnInit { ngOnInit() { if (this.data && this.data.file) { - this.selectedFile = this.data.file; - this.updatePreview(); + if (typeof this.data.file === 'string') { + this.previewUrl = this.data.file; + this.selectedFile = null; // It's just a URL, no file object yet + } else if (this.data.file instanceof File || this.data.file instanceof Blob) { + this.selectedFile = this.data.file; + this.updatePreview(); + } } } @@ -27,16 +32,20 @@ export class ImagePreviewDialogComponent implements OnInit { if (event.target.files && event.target.files[0]) { this.selectedFile = event.target.files[0]; this.updatePreview(); - this.fileInput.nativeElement.value = ''; + if (this.fileInput.nativeElement) { + this.fileInput.nativeElement.value = ''; + } } } updatePreview() { - const reader = new FileReader(); - reader.onload = () => { - this.previewUrl = reader.result; - }; - reader.readAsDataURL(this.selectedFile); + if (this.selectedFile) { + const reader = new FileReader(); + reader.onload = () => { + this.previewUrl = reader.result; + }; + reader.readAsDataURL(this.selectedFile); + } } confirm() { @@ -49,6 +58,8 @@ export class ImagePreviewDialogComponent implements OnInit { } close() { - this.dialogRef.close(); + // When closing without confirming, return undefined so the calling component knows no change was made + this.dialogRef.close(undefined); } } + From 8bc111eb6e0117b03a1278d399d7bcf8a50d86a0 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:13:43 -0500 Subject: [PATCH 21/48] commit --- .../certifications-add.component.html | 2 +- .../certifications-add.component.scss | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 930552c7f0..73534af572 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -12,7 +12,7 @@

Certificate Template Preview:

- Certificate Template Preview + Certificate Template Preview
diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index e69de29bb2..c2db0ec019 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -0,0 +1,16 @@ +.attachment-preview { + text-align: center; + margin-bottom: 1rem; +} + +.attachment-preview img { + max-width: 200px; + cursor: pointer; + border: 1px solid #ccc; + padding: 5px; + border-radius: 4px; +} + +.action-buttons { + margin-top: 1rem; +} From 18eeb84f3962efbff108b681e5f12b440f1ed5c4 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:19:54 -0500 Subject: [PATCH 22/48] commit --- .../certifications/certifications-add.component.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 73534af572..000a612fcc 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,10 +10,6 @@ {pageType, select, Add {Add} Update {Update}} Certification
-
-

Certificate Template Preview:

- Certificate Template Preview -
From f531f56e5fa886adbf02a94f41811e6ee3c65ba7 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:21:34 -0500 Subject: [PATCH 23/48] comit --- .../certifications-add.component.scss | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index c2db0ec019..e69de29bb2 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -1,16 +0,0 @@ -.attachment-preview { - text-align: center; - margin-bottom: 1rem; -} - -.attachment-preview img { - max-width: 200px; - cursor: pointer; - border: 1px solid #ccc; - padding: 5px; - border-radius: 4px; -} - -.action-buttons { - margin-top: 1rem; -} From 702195dba0f565407d1edb899cb2168652f27b53 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:23:06 -0500 Subject: [PATCH 24/48] commit --- .../certifications/certifications-add.component.scss | 0 .../certifications/certifications-add.component.ts | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/app/manager-dashboard/certifications/certifications-add.component.scss diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index b3c116921c..46f8b0c347 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -19,8 +19,7 @@ interface CertificationFormModel { } @Component({ - templateUrl: './certifications-add.component.html', - styleUrls: ['./certifications-add.component.scss'] + templateUrl: './certifications-add.component.html' }) export class CertificationsAddComponent implements OnInit, AfterViewChecked, CanComponentDeactivate { From f3d7fb882a37887f84a6bf1d39b060cfbfd4b4d8 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:09:58 -0500 Subject: [PATCH 25/48] this is for uploading and downloading certificate template --- .../certifications-add.component.html | 9 ++- .../certifications-add.component.scss | 0 .../certifications-add.component.ts | 14 +++- .../certifications/certifications.service.ts | 13 ++++ src/app/shared/couchdb.service.ts | 73 ++++++++++--------- 5 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 src/app/manager-dashboard/certifications/certifications-add.component.scss diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 103cc379a5..b8f11ac2c7 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,13 +15,16 @@ + + +
+
+
- -
-
+ diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 60b8de23c5..269a18ce6b 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -25,6 +25,8 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: string[] = []; pageType = 'Add'; disableRemove = true; + previewUrl: any; + selectedFile: any; @ViewChild(CoursesComponent) courseTable: CoursesComponent; constructor( @@ -78,6 +80,15 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.router.navigate([ navigation ], { relativeTo: this.route }); } + onFileSelected(event: any) { + if (event.target.files && event.target.files[0]) { + this.selectedFile = event.target.files[0]; + const reader = new FileReader(); + reader.onload = e => this.previewUrl = reader.result; + reader.readAsDataURL(this.selectedFile); + } + } + submitCertificate(reroute: boolean) { if (!this.certificateForm.valid) { showFormErrors(this.certificateForm.controls); @@ -87,7 +98,8 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificationsService.addCertification({ ...this.certificateInfo, ...certificateFormValue, - courseIds: this.courseIds + courseIds: this.courseIds, + attachment: this.selectedFile }).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index eb0e0a7eb3..9bd5fb3c27 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -4,6 +4,7 @@ import { PlanetMessageService } from '../../shared/planet-message.service'; import { MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; import { DialogsPromptComponent } from '../../shared/dialogs/dialogs-prompt.component'; import { dedupeShelfReduce } from '../../shared/utils'; +import { switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -52,6 +53,18 @@ export class CertificationsService { } addCertification(certification) { + const { attachment, ...cert } = certification; + if (attachment) { + return this.couchService.updateDocument(this.dbName, cert).pipe( + switchMap((res: any) => { + return this.couchService.putAttachment( + `${this.dbName}/${res.id}/attachment`, + attachment, + { rev: res.rev } + ); + }) + ); + } return this.couchService.updateDocument(this.dbName, { ...certification }); } diff --git a/src/app/shared/couchdb.service.ts b/src/app/shared/couchdb.service.ts index 7a41bd9bfa..ec0f08e559 100644 --- a/src/app/shared/couchdb.service.ts +++ b/src/app/shared/couchdb.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient, HttpRequest } from '@angular/common/http'; import { environment } from '../../environments/environment'; -import { Observable, of, empty, throwError, forkJoin } from 'rxjs'; -import { catchError, map, expand, toArray, flatMap, switchMap } from 'rxjs/operators'; +import { Observable, of, empty, throwError, forkJoin } from 'rxjs'; +import { catchError, map, expand, toArray, flatMap, switchMap } from 'rxjs/operators'; import { debug } from '../debug-operator'; import { PlanetMessageService } from './planet-message.service'; import { findDocuments } from './mangoQueries'; @@ -28,7 +28,8 @@ export class CouchService { const url = (domain ? (protocol || environment.parentProtocol) + '://' + domain : this.baseUrl) + '/' + db; let httpReq: Observable; if (type === 'post' || type === 'put') { - httpReq = this.http[type](url, data, opts); + const body = typeof data === 'object' && !(data instanceof Blob) ? JSON.stringify(data) : data; + httpReq = this.http[type](url, body, opts); } else { httpReq = this.http[type](url, opts); } @@ -68,10 +69,16 @@ export class CouchService { return this.couchDBReq('delete', db, this.setOpts(opts)); } - putAttachment(db: string, file: FormData, opts?: any) { - return this.couchDBReq('put', db, this.setOpts(opts), file); + putAttachment(db: string, file: File, opts?: any) { + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': file.type, + }), + withCredentials: true, + ...opts, + }; + return this.couchDBReq('put', db, this.setOpts(httpOptions), file); } - updateDocument(db: string, doc: any, opts?: any) { let docWithDate: any; return this.currentTime().pipe( @@ -97,33 +104,33 @@ export class CouchService { })); } - findAll(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { - return this.findAllRequest(db, query, opts).pipe(flatMap(({ docs }) => docs), toArray()); - } - - findAllStream(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { - return this.findAllRequest(db, query, opts).pipe(map(({ docs }) => docs)); - } - - findAttachmentsByIds(ids: string[], opts?: any) { - const uniqueIds = Array.from(new Set((ids || []).filter(id => !!id))); - if (uniqueIds.length === 0) { - return of([]); - } - const chunkSize = 50; - const queries = []; - for (let i = 0; i < uniqueIds.length; i += chunkSize) { - const chunk = uniqueIds.slice(i, i + chunkSize); - queries.push( - this.findAll('attachments', findDocuments({ '_id': { '$in': chunk } }, 0, 0, chunk.length), opts) - ); - } - return forkJoin(queries).pipe( - map((results: any[]) => results.reduce((acc: any[], docs: any[]) => acc.concat(docs), [])) - ); - } - - private findAllRequest(db: string, query: any, opts: any) { + findAll(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { + return this.findAllRequest(db, query, opts).pipe(flatMap(({ docs }) => docs), toArray()); + } + + findAllStream(db: string, query: any = { 'selector': { '_id': { '$gt': null } }, 'limit': 1000 }, opts?: any) { + return this.findAllRequest(db, query, opts).pipe(map(({ docs }) => docs)); + } + + findAttachmentsByIds(ids: string[], opts?: any) { + const uniqueIds = Array.from(new Set((ids || []).filter(id => !!id))); + if (uniqueIds.length === 0) { + return of([]); + } + const chunkSize = 50; + const queries = []; + for (let i = 0; i < uniqueIds.length; i += chunkSize) { + const chunk = uniqueIds.slice(i, i + chunkSize); + queries.push( + this.findAll('attachments', findDocuments({ '_id': { '$in': chunk } }, 0, 0, chunk.length), opts) + ); + } + return forkJoin(queries).pipe( + map((results: any[]) => results.reduce((acc: any[], docs: any[]) => acc.concat(docs), [])) + ); + } + + private findAllRequest(db: string, query: any, opts: any) { return this.post(db + '/_find', query, opts).pipe( catchError(() => { return of({ docs: [], rows: [] }); From 20e9cb5c3353a176fea9a3c53704d6c0978b76f5 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:17:16 -0500 Subject: [PATCH 26/48] commit to preview uploaded template --- .../certifications-add.component.html | 8 +-- .../certifications-add.component.ts | 19 ++++--- .../image-preview-dialog.component.html | 17 +++++++ .../dialogs/image-preview-dialog.component.ts | 50 +++++++++++++++++++ .../shared/dialogs/planet-dialogs.module.ts | 7 ++- 5 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 src/app/shared/dialogs/image-preview-dialog.component.html create mode 100644 src/app/shared/dialogs/image-preview-dialog.component.ts diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index b8f11ac2c7..1b3ff239d7 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,11 +15,7 @@ - - -
-
- +
@@ -27,4 +23,4 @@ - +
diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 269a18ce6b..b08dc0ba9b 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -9,6 +9,7 @@ import { CoursesComponent } from '../../courses/courses.component'; import { showFormErrors } from '../../shared/table-helpers'; import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; +import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; interface CertificationFormModel { name: string; @@ -25,7 +26,6 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: string[] = []; pageType = 'Add'; disableRemove = true; - previewUrl: any; selectedFile: any; @ViewChild(CoursesComponent) courseTable: CoursesComponent; @@ -80,13 +80,16 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.router.navigate([ navigation ], { relativeTo: this.route }); } - onFileSelected(event: any) { - if (event.target.files && event.target.files[0]) { - this.selectedFile = event.target.files[0]; - const reader = new FileReader(); - reader.onload = e => this.previewUrl = reader.result; - reader.readAsDataURL(this.selectedFile); - } + openImagePreviewDialog() { + const dialogRef = this.dialog.open(ImagePreviewDialogComponent, { + data: { file: this.selectedFile } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result !== undefined) { + this.selectedFile = result; + } + }); } submitCertificate(reroute: boolean) { diff --git a/src/app/shared/dialogs/image-preview-dialog.component.html b/src/app/shared/dialogs/image-preview-dialog.component.html new file mode 100644 index 0000000000..e0a0562a29 --- /dev/null +++ b/src/app/shared/dialogs/image-preview-dialog.component.html @@ -0,0 +1,17 @@ +

Image Preview

+ +
+ +
+ +

No image selected.

+
+
+ + + + + + + + diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts new file mode 100644 index 0000000000..52cb90ae93 --- /dev/null +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -0,0 +1,50 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; + +@Component({ + templateUrl: './image-preview-dialog.component.html', +}) +export class ImagePreviewDialogComponent implements OnInit { + + previewUrl: any; + selectedFile: File; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) {} + + ngOnInit() { + if (this.data && this.data.file) { + this.selectedFile = this.data.file; + this.updatePreview(); + } + } + + onFileSelected(event: any) { + if (event.target.files && event.target.files[0]) { + this.selectedFile = event.target.files[0]; + this.updatePreview(); + } + } + + updatePreview() { + const reader = new FileReader(); + reader.onload = () => { + this.previewUrl = reader.result; + }; + reader.readAsDataURL(this.selectedFile); + } + + confirm() { + this.dialogRef.close(this.selectedFile); + } + + remove() { + this.dialogRef.close(null); + } + + close() { + this.dialogRef.close(); + } +} diff --git a/src/app/shared/dialogs/planet-dialogs.module.ts b/src/app/shared/dialogs/planet-dialogs.module.ts index 89162811f4..550f63cc06 100644 --- a/src/app/shared/dialogs/planet-dialogs.module.ts +++ b/src/app/shared/dialogs/planet-dialogs.module.ts @@ -17,6 +17,7 @@ import { SyncDirective } from '../../manager-dashboard/sync.directive'; import { DialogsImagesComponent } from './dialogs-images.component'; import { DialogsAnnouncementComponent, DialogsAnnouncementSuccessComponent } from './dialogs-announcement.component'; import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-ratings.component'; +import { ImagePreviewDialogComponent } from './image-preview-dialog.component'; @NgModule({ imports: [ @@ -38,7 +39,8 @@ import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-rati DialogsRatingsComponent, DialogsRatingsDirective, ChangePasswordDirective, - SyncDirective + SyncDirective, + ImagePreviewDialogComponent ], declarations: [ DialogsFormComponent, @@ -53,7 +55,8 @@ import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-rati ChangePasswordDirective, SyncDirective, DialogsAnnouncementComponent, - DialogsAnnouncementSuccessComponent + DialogsAnnouncementSuccessComponent, + ImagePreviewDialogComponent ], providers: [ DialogsFormService, From 3fa485b26449c8e6e089fe2a1005268c27a3bd59 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:18:40 -0500 Subject: [PATCH 27/48] commit --- .../certifications/certifications-add.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1b3ff239d7..1d16415db4 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,7 +15,7 @@ - +
From c7bf00348d93c98e0338d2ab582689a66b65eb30 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:25:14 -0500 Subject: [PATCH 28/48] enhanced css for styling. --- .../image-preview-dialog.component.scss | 73 +++++++++++++++++++ .../dialogs/image-preview-dialog.component.ts | 1 + 2 files changed, 74 insertions(+) create mode 100644 src/app/shared/dialogs/image-preview-dialog.component.scss diff --git a/src/app/shared/dialogs/image-preview-dialog.component.scss b/src/app/shared/dialogs/image-preview-dialog.component.scss new file mode 100644 index 0000000000..01094880fb --- /dev/null +++ b/src/app/shared/dialogs/image-preview-dialog.component.scss @@ -0,0 +1,73 @@ +/* Dialog content layout */ +.mat-dialog-content { + display: flex; + justify-content: center; + align-items: center; + padding: 24px; + max-height: 70vh; + overflow: auto; +} + +/* Dialog action buttons */ +.mat-dialog-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; + padding: 16px 24px 24px; + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +/* Image preview styling */ +img { + max-width: 100%; + max-height: 60vh; + height: auto; + border-radius: 8px; + border: 1px solid #e0e0e0; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +/* Subtle hover effect for images */ +img:hover { + transform: scale(1.01); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); +} + +/* Material buttons spacing */ +.mat-button, +.mat-raised-button { + min-width: 96px; +} + +/* Primary raised button enhancement */ +.mat-raised-button.mat-primary { + background-color: #3f51b5; /* Material Indigo */ + color: #fff; + font-weight: 500; + box-shadow: 0 3px 6px rgba(63, 81, 181, 0.3); + transition: background-color 0.2s ease, box-shadow 0.2s ease; +} + +.mat-raised-button.mat-primary:hover { + background-color: #35449a; + box-shadow: 0 6px 12px rgba(63, 81, 181, 0.4); +} + +/* Mobile responsiveness */ +@media (max-width: 600px) { + .mat-dialog-content { + padding: 16px; + } + + .mat-dialog-actions { + padding: 12px 16px 16px; + flex-direction: column; + align-items: stretch; + } + + .mat-dialog-actions button { + width: 100%; + } +} diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index 52cb90ae93..25bd39b7ab 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -3,6 +3,7 @@ import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALO @Component({ templateUrl: './image-preview-dialog.component.html', + styleUrls: ['./image-preview-dialog.component.scss'] }) export class ImagePreviewDialogComponent implements OnInit { From bd17bfce3491204c0e8e7d25e72c6ee7b530a121 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:27:48 -0500 Subject: [PATCH 29/48] enhancement to remove button --- src/app/shared/dialogs/image-preview-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index 25bd39b7ab..ac4484acda 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -42,7 +42,8 @@ export class ImagePreviewDialogComponent implements OnInit { } remove() { - this.dialogRef.close(null); + this.selectedFile = null; + this.previewUrl = null; } close() { From 71f63ed9aa56453d4a5d2d831db6b9ac34c39fcc Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:30:12 -0500 Subject: [PATCH 30/48] commit --- src/app/shared/dialogs/image-preview-dialog.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index ac4484acda..13c0fd9998 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnInit, ViewChild, ElementRef } from '@angular/core'; import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; @Component({ @@ -9,6 +9,7 @@ export class ImagePreviewDialogComponent implements OnInit { previewUrl: any; selectedFile: File; + @ViewChild('fileInput') fileInput: ElementRef; constructor( public dialogRef: MatDialogRef, @@ -26,6 +27,7 @@ export class ImagePreviewDialogComponent implements OnInit { if (event.target.files && event.target.files[0]) { this.selectedFile = event.target.files[0]; this.updatePreview(); + this.fileInput.nativeElement.value = ''; } } From 2db8cb7dbd3d857f8d5ba6187f7020b6fa13793d Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:34:53 -0500 Subject: [PATCH 31/48] commit --- .../certifications-add.component.ts | 36 ++++++++++++++++++- .../image-preview-dialog.component.html | 2 +- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index b08dc0ba9b..84f75061ac 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -59,10 +59,12 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; + this.loadAttachment(); }); } else { - this.certificateInfo._id = undefined; + this.certificateInfo._id = `temp-${Date.now()}`; this.courseIds = []; + this.loadAttachment(); } }); } @@ -88,10 +90,41 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { this.selectedFile = result; + this.saveAttachment(); } }); } + loadAttachment() { + const storedAttachment = localStorage.getItem(`certification-attachment-${this.certificateInfo._id}`); + if (storedAttachment) { + const attachment = JSON.parse(storedAttachment); + const byteCharacters = atob(attachment.data.split(',')[1]); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + this.selectedFile = new File([byteArray], attachment.name, { type: attachment.type }); + } + } + + saveAttachment() { + if (this.selectedFile) { + const reader = new FileReader(); + reader.onload = () => { + localStorage.setItem(`certification-attachment-${this.certificateInfo._id}`, JSON.stringify({ + name: this.selectedFile.name, + type: this.selectedFile.type, + data: reader.result + })); + }; + reader.readAsDataURL(this.selectedFile); + } else { + localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); + } + } + submitCertificate(reroute: boolean) { if (!this.certificateForm.valid) { showFormErrors(this.certificateForm.controls); @@ -104,6 +137,7 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: this.courseIds, attachment: this.selectedFile }).subscribe((res) => { + localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` diff --git a/src/app/shared/dialogs/image-preview-dialog.component.html b/src/app/shared/dialogs/image-preview-dialog.component.html index e0a0562a29..38260000ac 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.html +++ b/src/app/shared/dialogs/image-preview-dialog.component.html @@ -9,7 +9,7 @@

Image Preview

- + From b214b784c14d1f6b3d7e01470bd24c54f9fe4fca Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:30:31 -0500 Subject: [PATCH 32/48] commit --- .../certifications/certifications-add.component.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1d16415db4..a33fbda5ec 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,12 +15,14 @@ - +
+ - +
+ \ No newline at end of file From 195abbfa4b89cee2e61662bbe35e435b134db878 Mon Sep 17 00:00:00 2001 From: Emmanuel Baah Date: Thu, 18 Dec 2025 14:31:54 -0500 Subject: [PATCH 33/48] Update certifications-add.component.html --- .../certifications/certifications-add.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index a33fbda5ec..be0a8fe644 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -25,4 +25,4 @@ - \ No newline at end of file + From 5e2cd9fc3bd99873e3f63fe6eab1446b86c45dbc Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:34:02 -0500 Subject: [PATCH 34/48] commit --- .../certifications/certifications-add.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index be0a8fe644..1a0ed96df4 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -15,7 +15,7 @@ - +
From f1a5b0ecc1bbef029625afffc6cff189553f5c59 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:48:50 -0500 Subject: [PATCH 35/48] feat: Implement certificate background image from template Add getAttachment method to CouchService to enable fetching of attachment data from CouchDB. Modify UsersAchievementsComponent to fetch certificate template attachments and use them as background images in the generated PDF achievements. This addresses the feedback regarding using doc.addImage for efficiency. --- src/app/shared/couchdb.service.ts | 10 +++ .../users-achievements.component.ts | 63 +++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/app/shared/couchdb.service.ts b/src/app/shared/couchdb.service.ts index ec0f08e559..24564f72c2 100644 --- a/src/app/shared/couchdb.service.ts +++ b/src/app/shared/couchdb.service.ts @@ -232,4 +232,14 @@ export class CouchService { ); } + getAttachment(db: string, docId: string, attachmentId: string, opts?: any): Observable { + const url = `${this.baseUrl}/${db}/${docId}/${attachmentId}`; + const httpOptions = { + ...this.defaultOpts, + responseType: 'blob' as 'json', + ...opts, + }; + return this.couchDBReq('get', url, this.setOpts(httpOptions)); + } + } diff --git a/src/app/users/users-achievements/users-achievements.component.ts b/src/app/users/users-achievements/users-achievements.component.ts index 941cfec493..ee02ae0f26 100644 --- a/src/app/users/users-achievements/users-achievements.component.ts +++ b/src/app/users/users-achievements/users-achievements.component.ts @@ -5,8 +5,8 @@ import { CouchService } from '../../shared/couchdb.service'; import { UserService } from '../../shared/user.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { UsersAchievementsService } from './users-achievements.service'; -import { catchError, auditTime } from 'rxjs/operators'; -import { throwError, combineLatest } from 'rxjs'; +import { catchError, auditTime, switchMap, map } from 'rxjs/operators'; // Added switchMap and map +import { throwError, combineLatest, forkJoin, Observable, of } from 'rxjs'; // Added forkJoin, Observable, of import { StateService } from '../../shared/state.service'; import { CoursesService } from '../../courses/courses.service'; import { environment } from '../../../environments/environment'; @@ -124,12 +124,52 @@ export class UsersAchievementsComponent implements OnInit { } setCertifications(courses = [], progress = [], certifications = []) { - this.certifications = certifications.filter(certification => { + const filteredCertifications = certifications.filter(certification => { const certificateCourses = courses .filter(course => certification.courseIds.indexOf(course._id) > -1) .map(course => ({ ...course, progress: progress.filter(p => p.courseId === course._id) })); return certificateCourses.every(course => this.certificationsService.isCourseCompleted(course, this.user)); }); + + const attachmentFetches: Observable[] = filteredCertifications.map(certification => { + // Check if _attachments exists and has an attachment named 'attachment' + if (certification._attachments && certification._attachments.attachment) { + return this.couchService.getAttachment(this.certificationsService.dbName, certification._id, 'attachment').pipe( + switchMap(blob => { + return new Observable(observer => { + const reader = new FileReader(); + reader.onloadend = () => { + // Ensure reader.result is a string (base64) before assigning + certification.templateImage = typeof reader.result === 'string' ? reader.result : null; + observer.next(certification); + observer.complete(); + }; + reader.onerror = (error) => { + console.error('Error reading blob as DataURL', error); + certification.templateImage = null; // Set to null on error + observer.next(certification); + observer.complete(); + }; + reader.readAsDataURL(blob); + }); + }), + catchError(err => { + console.error('Error fetching attachment for certification', certification._id, err); + return of(certification); // Return certification without attachment if error + }) + ); + } else { + return of(certification); // No attachment, return certification as is + } + }); + + if (attachmentFetches.length > 0) { + forkJoin(attachmentFetches).subscribe(certsWithAttachments => { + this.certifications = certsWithAttachments; + }); + } else { + this.certifications = filteredCertifications; + } } copyLink() { @@ -232,7 +272,7 @@ export class UsersAchievementsComponent implements OnInit { contentArray = contentArray.concat(optionals); - const documentDefinition = { + let documentDefinition: any = { // Changed to let to allow modification content: contentArray, styles: { header: { @@ -246,6 +286,21 @@ export class UsersAchievementsComponent implements OnInit { }, }; + // Find a certification with a template image + const certificationWithTemplate = this.certifications.find(cert => cert.templateImage); + + if (certificationWithTemplate && certificationWithTemplate.templateImage) { + documentDefinition.background = [ + { + image: certificationWithTemplate.templateImage, + width: 612, // US Letter width in points (8.5 inches * 72 points/inch) + height: 792, // US Letter height in points (11 inches * 72 points/inch) + absolutePosition: { x: 0, y: 0 }, + opacity: 0.5 // Optional: adjust opacity for readability + } + ]; + } + pdfMake .createPdf(documentDefinition) .download($localize`${this.user.name} achievements.pdf`); From 431da01fb368712217757cdc2e9a68cd1b991531 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:53:17 -0500 Subject: [PATCH 36/48] refactor: Remove localStorage for certificate attachments Refactor CertificationsAddComponent to remove the usage of localStorage for temporarily storing certificate attachments. The component now fetches attachments directly from CouchDB when editing an existing certification. The selected file is held in memory until the form is submitted. --- .../certifications-add.component.ts | 40 +++---------------- .../certifications/certifications.service.ts | 4 ++ 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 84f75061ac..ac9d64ed19 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -59,12 +59,16 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; - this.loadAttachment(); + // Load attachment from CouchDB if it exists + if (certification._attachments && certification._attachments.attachment) { + this.certificationsService.getAttachment(id, 'attachment').subscribe(blob => { + this.selectedFile = new File([blob], 'attachment'); // Create a File object + }); + } }); } else { this.certificateInfo._id = `temp-${Date.now()}`; this.courseIds = []; - this.loadAttachment(); } }); } @@ -90,41 +94,10 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { this.selectedFile = result; - this.saveAttachment(); } }); } - loadAttachment() { - const storedAttachment = localStorage.getItem(`certification-attachment-${this.certificateInfo._id}`); - if (storedAttachment) { - const attachment = JSON.parse(storedAttachment); - const byteCharacters = atob(attachment.data.split(',')[1]); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - this.selectedFile = new File([byteArray], attachment.name, { type: attachment.type }); - } - } - - saveAttachment() { - if (this.selectedFile) { - const reader = new FileReader(); - reader.onload = () => { - localStorage.setItem(`certification-attachment-${this.certificateInfo._id}`, JSON.stringify({ - name: this.selectedFile.name, - type: this.selectedFile.type, - data: reader.result - })); - }; - reader.readAsDataURL(this.selectedFile); - } else { - localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); - } - } - submitCertificate(reroute: boolean) { if (!this.certificateForm.valid) { showFormErrors(this.certificateForm.controls); @@ -137,7 +110,6 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: this.courseIds, attachment: this.selectedFile }).subscribe((res) => { - localStorage.removeItem(`certification-attachment-${this.certificateInfo._id}`); this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index 9bd5fb3c27..800f9364bd 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -28,6 +28,10 @@ export class CertificationsService { return this.couchService.get(`${this.dbName}/${id}`); } + getAttachment(docId: string, attachmentId: string) { + return this.couchService.getAttachment(this.dbName, docId, attachmentId); + } + openDeleteDialog(certification: any, callback) { const displayName = certification.name; this.deleteDialog = this.dialog.open(DialogsPromptComponent, { From 4839ec3044532c02c62687a2914486b3517c5178 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:00:01 -0500 Subject: [PATCH 37/48] feat: Persist certificate attachments in CouchDB on selection Implement a 'draft' workflow for certificate attachments. When a user selects a file for a new certificate, a draft document is created in CouchDB and the file is uploaded as an attachment. This ensures that the selected attachment persists across page reloads and navigations, using CouchDB as the storage backend instead of localStorage. --- .../certifications-add.component.ts | 39 ++++++++++++++++--- .../certifications/certifications.service.ts | 12 ++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index ac9d64ed19..5e887d3569 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -10,6 +10,7 @@ import { showFormErrors } from '../../shared/table-helpers'; import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; +import { switchMap } from 'rxjs/operators'; interface CertificationFormModel { name: string; @@ -66,9 +67,6 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { }); } }); - } else { - this.certificateInfo._id = `temp-${Date.now()}`; - this.courseIds = []; } }); } @@ -94,6 +92,33 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { this.selectedFile = result; + // If it's a new certification (no _id from route), create a draft. + if (!this.route.snapshot.paramMap.get('id')) { + // If no doc exists yet, create one + if (!this.certificateInfo._id || this.certificateInfo._id.startsWith('temp-')) { + this.certificationsService.createDraftCertification().pipe( + switchMap((res: any) => { + this.certificateInfo = { _id: res.id, _rev: res.rev }; + return this.certificationsService.uploadAttachment(res.id, res.rev, this.selectedFile); + }) + ).subscribe(uploadRes => { + this.certificateInfo._rev = uploadRes.rev; // Update rev after attachment upload + this.planetMessageService.showMessage($localize`Draft saved.`); + }); + } else { // Draft doc already exists, just upload attachment + this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) + .subscribe(uploadRes => { + this.certificateInfo._rev = uploadRes.rev; + this.planetMessageService.showMessage($localize`Draft updated.`); + }); + } + } else { // Existing certification, just upload the new attachment + this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) + .subscribe(uploadRes => { + this.certificateInfo._rev = uploadRes.rev; + this.planetMessageService.showMessage($localize`Attachment updated.`); + }); + } } }); } @@ -104,12 +129,14 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { return; } const certificateFormValue: CertificationFormModel = this.certificateForm.getRawValue(); - this.certificationsService.addCertification({ + const certData = { ...this.certificateInfo, ...certificateFormValue, courseIds: this.courseIds, - attachment: this.selectedFile - }).subscribe((res) => { + type: 'certification' // Ensure type is 'certification', not 'draft' + }; + // The attachment is already in CouchDB, so we just update the document fields. + this.certificationsService.addCertification(certData).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index 800f9364bd..9ec4aeebaf 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -72,6 +72,18 @@ export class CertificationsService { return this.couchService.updateDocument(this.dbName, { ...certification }); } + createDraftCertification() { + return this.couchService.updateDocument(this.dbName, { type: 'draft' }); + } + + uploadAttachment(docId: string, rev: string, file: File) { + return this.couchService.putAttachment( + `${this.dbName}/${docId}/attachment`, + file, + { rev } + ); + } + isCourseCompleted(course, user) { return course.doc.steps.length === course.progress .filter(step => step.userId === user._id && step.passed) From 5396de35ef7d7155b957068fa899afbeb393a343 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:20:05 -0500 Subject: [PATCH 38/48] fix(attachments): Correct getAttachment method in CouchService The getAttachment method was using couchDBReq with a full URL, which was incorrect. It was also casting the responseType of 'blob' as 'json'. This commit corrects the getAttachment method to use http.get directly with the correct responseType ('blob') and proper URL construction and error handling. This should resolve the issue of attachments not being loaded correctly on page refresh. --- src/app/shared/couchdb.service.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/shared/couchdb.service.ts b/src/app/shared/couchdb.service.ts index 24564f72c2..ee6b9c08b9 100644 --- a/src/app/shared/couchdb.service.ts +++ b/src/app/shared/couchdb.service.ts @@ -236,10 +236,20 @@ export class CouchService { const url = `${this.baseUrl}/${db}/${docId}/${attachmentId}`; const httpOptions = { ...this.defaultOpts, - responseType: 'blob' as 'json', + responseType: 'blob', ...opts, }; - return this.couchDBReq('get', url, this.setOpts(httpOptions)); + // Use http.get directly instead of couchDBReq because couchDBReq is not suitable for attachment URLs + return this.http.get(url, httpOptions).pipe( + catchError(err => { + if (err.status === 403) { + this.planetMessageService.showAlert($localize`You are not authorized. Please contact administrator.`); + } else { + this.planetMessageService.showAlert($localize`Error fetching attachment: ${err.message}`); + } + return throwError(err); + }) + ); } } From ed867e1d325d4ad8190ba035bc6021971d9216ec Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:36:35 -0500 Subject: [PATCH 39/48] refactor(certifications): Align attachment handling with edit profile Refactor certificate attachment handling to follow the same pattern as the user profile edit page. - Remove the 'draft' workflow and associated service methods. - Load existing attachments using a direct URL for previews. - Convert newly selected images to base64 and send them inline with the document on submission. This ensures that attachments are persisted correctly and are not lost on page refresh, addressing the user's feedback. --- .../certifications-add.component.ts | 72 +++++++++---------- .../certifications/certifications.service.ts | 31 +------- 2 files changed, 36 insertions(+), 67 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 5e887d3569..7cb2af7de5 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -10,7 +10,7 @@ import { showFormErrors } from '../../shared/table-helpers'; import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; -import { switchMap } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; interface CertificationFormModel { name: string; @@ -27,7 +27,10 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { courseIds: string[] = []; pageType = 'Add'; disableRemove = true; - selectedFile: any; + selectedFile: any; // Will hold base64 string for new files + previewSrc: any = 'assets/image.png'; // For image preview + urlPrefix = environment.couchAddress + '/' + this.dbName + '/'; + @ViewChild(CoursesComponent) courseTable: CoursesComponent; constructor( @@ -60,11 +63,9 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; - // Load attachment from CouchDB if it exists if (certification._attachments && certification._attachments.attachment) { - this.certificationsService.getAttachment(id, 'attachment').subscribe(blob => { - this.selectedFile = new File([blob], 'attachment'); // Create a File object - }); + // Construct direct URL for the preview + this.previewSrc = `${this.urlPrefix}${id}/attachment`; } }); } @@ -86,38 +87,23 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { openImagePreviewDialog() { const dialogRef = this.dialog.open(ImagePreviewDialogComponent, { - data: { file: this.selectedFile } + data: { file: this.previewSrc } // Pass previewSrc to the dialog }); dialogRef.afterClosed().subscribe(result => { if (result !== undefined) { - this.selectedFile = result; - // If it's a new certification (no _id from route), create a draft. - if (!this.route.snapshot.paramMap.get('id')) { - // If no doc exists yet, create one - if (!this.certificateInfo._id || this.certificateInfo._id.startsWith('temp-')) { - this.certificationsService.createDraftCertification().pipe( - switchMap((res: any) => { - this.certificateInfo = { _id: res.id, _rev: res.rev }; - return this.certificationsService.uploadAttachment(res.id, res.rev, this.selectedFile); - }) - ).subscribe(uploadRes => { - this.certificateInfo._rev = uploadRes.rev; // Update rev after attachment upload - this.planetMessageService.showMessage($localize`Draft saved.`); - }); - } else { // Draft doc already exists, just upload attachment - this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) - .subscribe(uploadRes => { - this.certificateInfo._rev = uploadRes.rev; - this.planetMessageService.showMessage($localize`Draft updated.`); - }); - } - } else { // Existing certification, just upload the new attachment - this.certificationsService.uploadAttachment(this.certificateInfo._id, this.certificateInfo._rev, this.selectedFile) - .subscribe(uploadRes => { - this.certificateInfo._rev = uploadRes.rev; - this.planetMessageService.showMessage($localize`Attachment updated.`); - }); + if (result instanceof File) { + // New file selected, convert to base64 + const reader = new FileReader(); + reader.onload = () => { + this.selectedFile = reader.result; // base64 string + this.previewSrc = reader.result; + }; + reader.readAsDataURL(result); + } else if (result === null) { + // Image removed + this.selectedFile = null; + this.previewSrc = 'assets/image.png'; } } }); @@ -129,13 +115,25 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { return; } const certificateFormValue: CertificationFormModel = this.certificateForm.getRawValue(); - const certData = { + const certData: any = { ...this.certificateInfo, ...certificateFormValue, courseIds: this.courseIds, - type: 'certification' // Ensure type is 'certification', not 'draft' }; - // The attachment is already in CouchDB, so we just update the document fields. + + if (this.selectedFile && this.selectedFile.startsWith('data:')) { + // New base64 image data is present + const imgDataArr: string[] = this.selectedFile.split(/;\w+,/); + const contentType: string = imgDataArr[0].replace(/data:/, ''); + const data: string = imgDataArr[1]; + certData._attachments = { + 'attachment': { + 'content_type': contentType, + 'data': data + } + }; + } + this.certificationsService.addCertification(certData).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; this.planetMessageService.showMessage( diff --git a/src/app/manager-dashboard/certifications/certifications.service.ts b/src/app/manager-dashboard/certifications/certifications.service.ts index 9ec4aeebaf..e2b4fe9f93 100644 --- a/src/app/manager-dashboard/certifications/certifications.service.ts +++ b/src/app/manager-dashboard/certifications/certifications.service.ts @@ -4,7 +4,6 @@ import { PlanetMessageService } from '../../shared/planet-message.service'; import { MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; import { DialogsPromptComponent } from '../../shared/dialogs/dialogs-prompt.component'; import { dedupeShelfReduce } from '../../shared/utils'; -import { switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -28,10 +27,6 @@ export class CertificationsService { return this.couchService.get(`${this.dbName}/${id}`); } - getAttachment(docId: string, attachmentId: string) { - return this.couchService.getAttachment(this.dbName, docId, attachmentId); - } - openDeleteDialog(certification: any, callback) { const displayName = certification.name; this.deleteDialog = this.dialog.open(DialogsPromptComponent, { @@ -57,31 +52,7 @@ export class CertificationsService { } addCertification(certification) { - const { attachment, ...cert } = certification; - if (attachment) { - return this.couchService.updateDocument(this.dbName, cert).pipe( - switchMap((res: any) => { - return this.couchService.putAttachment( - `${this.dbName}/${res.id}/attachment`, - attachment, - { rev: res.rev } - ); - }) - ); - } - return this.couchService.updateDocument(this.dbName, { ...certification }); - } - - createDraftCertification() { - return this.couchService.updateDocument(this.dbName, { type: 'draft' }); - } - - uploadAttachment(docId: string, rev: string, file: File) { - return this.couchService.putAttachment( - `${this.dbName}/${docId}/attachment`, - file, - { rev } - ); + return this.couchService.updateDocument(this.dbName, certification); } isCourseCompleted(course, user) { From 1d799f9a099c9826809a3a935e266cff4e764dd8 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:42:50 -0500 Subject: [PATCH 40/48] fix(certifications): Add image preview to certifications add/edit page Add an image preview to the template to display the certificate template. The preview is only shown if it's not the default placeholder image. Also, change the 'Upload' button text to 'Change Template' if an image is already present. This provides visual feedback to the user and resolves the issue of the attachment appearing to be lost on page refresh. --- .../certifications/certifications-add.component.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1a0ed96df4..930552c7f0 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,12 +10,19 @@ {pageType, select, Add {Add} Update {Update}} Certification
+
+

Certificate Template Preview:

+ Certificate Template Preview +
- +
From adad1dfcd6d44ca8efb51e36367d332338281214 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:47:05 -0500 Subject: [PATCH 41/48] revert: Remove on-page preview for certificate template Revert the change that added an on-page preview for the certificate template. The user has requested that the preview only be visible in the dialog. The underlying logic for attachment persistence remains in place. --- .../certifications/certifications-add.component.html | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 930552c7f0..1a0ed96df4 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,19 +10,12 @@ {pageType, select, Add {Add} Update {Update}} Certification
-
-

Certificate Template Preview:

- Certificate Template Preview -
- +
From d2b835b899f93caf5f780730911ffa4776f938a5 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 16:53:45 -0500 Subject: [PATCH 42/48] fix(certifications): Restore image preview to fix persistence issue Re-adds the image preview to the certificate add/edit page. This resolves the issue where the attachment appeared to be lost on page refresh because it was not being displayed. The component now shows a preview of the existing template on load, and updates the preview when a new template is selected. --- .../certifications/certifications-add.component.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 1a0ed96df4..930552c7f0 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,12 +10,19 @@ {pageType, select, Add {Add} Update {Update}} Certification
+
+

Certificate Template Preview:

+ Certificate Template Preview +
- +
From b13b75b559eb891d40f93993924b10da13fbab5e Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:01:01 -0500 Subject: [PATCH 43/48] commit --- .../certifications-add.component.scss | 115 ++++++++++++++++++ .../certifications-add.component.ts | 3 +- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index e69de29bb2..c26d6a3442 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -0,0 +1,115 @@ +/* ===== Toolbar ===== */ +mat-toolbar { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} + +.btnBack { + margin-right: 8px; +} + +/* ===== Page Container ===== */ +.space-container { + padding: 24px; + max-width: 1100px; + margin: 0 auto; +} + +/* ===== Section Header ===== */ +.space-container > mat-toolbar { + border-radius: 10px; + margin-bottom: 24px; + padding: 12px 20px; + font-size: 1.1rem; +} + +/* ===== Main Content Card ===== */ +.view-container { + background: #ffffff; + border-radius: 16px; + padding: 28px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +/* ===== Certificate Preview ===== */ +.attachment-preview { + margin-bottom: 24px; + padding: 16px; + border-radius: 12px; + background: #f9fafb; + border: 1px dashed #d1d5db; + text-align: center; +} + +.attachment-preview p { + margin-bottom: 12px; + font-weight: 500; + color: #374151; +} + +.attachment-preview img { + max-width: 220px; + border-radius: 8px; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.attachment-preview img:hover { + transform: scale(1.03); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15); +} + +/* ===== Action Buttons ===== */ +.action-buttons { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 28px; +} + +.action-buttons button { + border-radius: 24px; + padding: 8px 20px; + font-weight: 500; +} + +/* Primary emphasis */ +.action-buttons button[color="primary"] { + box-shadow: 0 4px 12px rgba(25, 118, 210, 0.25); +} + +/* ===== Form ===== */ +.full-width { + width: 100%; + margin-bottom: 24px; +} + +mat-form-field { + font-size: 1rem; +} + +/* ===== Courses Section ===== */ +planet-courses { + display: block; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #e5e7eb; +} + +/* ===== Responsive Tweaks ===== */ +@media (max-width: 768px) { + .space-container { + padding: 16px; + } + + .view-container { + padding: 20px; + } + + .action-buttons { + flex-direction: column; + align-items: stretch; + } +} diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index 7cb2af7de5..f6af960108 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -17,7 +17,8 @@ interface CertificationFormModel { } @Component({ - templateUrl: './certifications-add.component.html' + templateUrl: './certifications-add.component.html', + styleUrls: ['./certifications-add.component.scss'] }) export class CertificationsAddComponent implements OnInit, AfterViewChecked { From 7a59fdb05811160b4d1548f76701a98b2d1de987 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:07:49 -0500 Subject: [PATCH 44/48] fix(certifications): Implement robust attachment handling Refactor the certificate add/edit component to correctly handle image attachments, preventing data loss on page refresh. - The component now displays a preview of the existing certificate template on load. - Implemented an 'unsaved changes' guard to prevent accidental navigation away from the page with unsaved modifications to the form or the attachment. - The image preview dialog is now more robust and can handle both File objects and URL strings. - Aligned the attachment handling pattern with the existing 'edit profile' functionality for consistency. --- .../certifications-add.component.scss | 115 ------------------ .../certifications-add.component.ts | 82 ++++++++++--- .../dialogs/image-preview-dialog.component.ts | 31 +++-- 3 files changed, 87 insertions(+), 141 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index c26d6a3442..e69de29bb2 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -1,115 +0,0 @@ -/* ===== Toolbar ===== */ -mat-toolbar { - display: flex; - align-items: center; - gap: 8px; - font-weight: 500; -} - -.btnBack { - margin-right: 8px; -} - -/* ===== Page Container ===== */ -.space-container { - padding: 24px; - max-width: 1100px; - margin: 0 auto; -} - -/* ===== Section Header ===== */ -.space-container > mat-toolbar { - border-radius: 10px; - margin-bottom: 24px; - padding: 12px 20px; - font-size: 1.1rem; -} - -/* ===== Main Content Card ===== */ -.view-container { - background: #ffffff; - border-radius: 16px; - padding: 28px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); -} - -/* ===== Certificate Preview ===== */ -.attachment-preview { - margin-bottom: 24px; - padding: 16px; - border-radius: 12px; - background: #f9fafb; - border: 1px dashed #d1d5db; - text-align: center; -} - -.attachment-preview p { - margin-bottom: 12px; - font-weight: 500; - color: #374151; -} - -.attachment-preview img { - max-width: 220px; - border-radius: 8px; - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.attachment-preview img:hover { - transform: scale(1.03); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15); -} - -/* ===== Action Buttons ===== */ -.action-buttons { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-bottom: 28px; -} - -.action-buttons button { - border-radius: 24px; - padding: 8px 20px; - font-weight: 500; -} - -/* Primary emphasis */ -.action-buttons button[color="primary"] { - box-shadow: 0 4px 12px rgba(25, 118, 210, 0.25); -} - -/* ===== Form ===== */ -.full-width { - width: 100%; - margin-bottom: 24px; -} - -mat-form-field { - font-size: 1rem; -} - -/* ===== Courses Section ===== */ -planet-courses { - display: block; - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid #e5e7eb; -} - -/* ===== Responsive Tweaks ===== */ -@media (max-width: 768px) { - .space-container { - padding: 16px; - } - - .view-container { - padding: 20px; - } - - .action-buttons { - flex-direction: column; - align-items: stretch; - } -} diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index f6af960108..b3c116921c 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild, AfterViewChecked, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, ViewChild, AfterViewChecked, ChangeDetectorRef, HostListener } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; @@ -11,6 +11,8 @@ import { ValidatorService } from '../../validators/validator.service'; import { PlanetMessageService } from '../../shared/planet-message.service'; import { ImagePreviewDialogComponent } from '../../shared/dialogs/image-preview-dialog.component'; import { environment } from '../../../environments/environment'; +import { CanComponentDeactivate } from '../../shared/unsaved-changes.guard'; +import { warningMsg } from '../../shared/unsaved-changes.component'; interface CertificationFormModel { name: string; @@ -20,7 +22,7 @@ interface CertificationFormModel { templateUrl: './certifications-add.component.html', styleUrls: ['./certifications-add.component.scss'] }) -export class CertificationsAddComponent implements OnInit, AfterViewChecked { +export class CertificationsAddComponent implements OnInit, AfterViewChecked, CanComponentDeactivate { readonly dbName = 'certifications'; certificateInfo: { _id?: string, _rev?: string } = {}; @@ -32,6 +34,10 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { previewSrc: any = 'assets/image.png'; // For image preview urlPrefix = environment.couchAddress + '/' + this.dbName + '/'; + initialFormValues: any; + attachmentChanged = false; + isFormInitialized = false; + @ViewChild(CoursesComponent) courseTable: CoursesComponent; constructor( @@ -61,6 +67,7 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.certificateInfo._id = id; this.certificationsService.getCertification(id).subscribe(certification => { this.certificateForm.patchValue({ name: certification.name || '' } as Partial); + this.initialFormValues = { ...this.certificateForm.value }; this.certificateInfo._rev = certification._rev; this.courseIds = certification.courseIds || []; this.pageType = 'Update'; @@ -68,11 +75,23 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { // Construct direct URL for the preview this.previewSrc = `${this.urlPrefix}${id}/attachment`; } + this.isFormInitialized = true; + this.setupFormValueChanges(); }); + } else { + this.initialFormValues = { ...this.certificateForm.value }; + this.isFormInitialized = true; + this.setupFormValueChanges(); } }); } + setupFormValueChanges() { + this.certificateForm.valueChanges.subscribe(() => { + // The guard will check for changes, no need to set a flag here + }); + } + ngAfterViewChecked() { const disableRemove = !this.courseTable || !this.courseTable.selection.selected.length; if (this.disableRemove !== disableRemove) { @@ -92,20 +111,22 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { }); dialogRef.afterClosed().subscribe(result => { - if (result !== undefined) { - if (result instanceof File) { - // New file selected, convert to base64 - const reader = new FileReader(); - reader.onload = () => { - this.selectedFile = reader.result; // base64 string - this.previewSrc = reader.result; - }; - reader.readAsDataURL(result); - } else if (result === null) { - // Image removed - this.selectedFile = null; - this.previewSrc = 'assets/image.png'; - } + if (result === undefined) { + return; // Dialog was closed without action + } + this.attachmentChanged = true; // Any confirmed action in the dialog is a change + if (result instanceof File) { + // New file selected, convert to base64 + const reader = new FileReader(); + reader.onload = () => { + this.selectedFile = reader.result as string; + this.previewSrc = this.selectedFile; + }; + reader.readAsDataURL(result); + } else if (result === null) { + // Image removed + this.selectedFile = null; + this.previewSrc = 'assets/image.png'; } }); } @@ -133,10 +154,18 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { 'data': data } }; + } else if (this.attachmentChanged && !this.selectedFile) { + // This means the image was removed + if (certData._attachments) { + delete certData._attachments; + } } + this.certificationsService.addCertification(certData).subscribe((res) => { this.certificateInfo = { _id: res.id, _rev: res.rev }; + this.initialFormValues = { ...this.certificateForm.value }; + this.attachmentChanged = false; this.planetMessageService.showMessage( this.pageType === 'Add' ? $localize`New certification added` : $localize`Certification updated` ); @@ -166,4 +195,25 @@ export class CertificationsAddComponent implements OnInit, AfterViewChecked { this.courseIds = this.courseIds.filter(id => this.courseTable.selection.selected.indexOf(id) === -1); } + canDeactivate(): boolean { + return !this.getHasUnsavedChanges(); + } + + isFormPristine(): boolean { + return JSON.stringify(this.certificateForm.value) === JSON.stringify(this.initialFormValues); + } + + @HostListener('window:beforeunload', [ '$event' ]) + unloadNotification($event: BeforeUnloadEvent): void { + if (this.getHasUnsavedChanges()) { + $event.returnValue = warningMsg; + } + } + + private getHasUnsavedChanges(): boolean { + if (!this.isFormInitialized) { + return false; + } + return !this.isFormPristine() || this.attachmentChanged; + } } diff --git a/src/app/shared/dialogs/image-preview-dialog.component.ts b/src/app/shared/dialogs/image-preview-dialog.component.ts index 13c0fd9998..c55cf97947 100644 --- a/src/app/shared/dialogs/image-preview-dialog.component.ts +++ b/src/app/shared/dialogs/image-preview-dialog.component.ts @@ -8,7 +8,7 @@ import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALO export class ImagePreviewDialogComponent implements OnInit { previewUrl: any; - selectedFile: File; + selectedFile: File | null; @ViewChild('fileInput') fileInput: ElementRef; constructor( @@ -18,8 +18,13 @@ export class ImagePreviewDialogComponent implements OnInit { ngOnInit() { if (this.data && this.data.file) { - this.selectedFile = this.data.file; - this.updatePreview(); + if (typeof this.data.file === 'string') { + this.previewUrl = this.data.file; + this.selectedFile = null; // It's just a URL, no file object yet + } else if (this.data.file instanceof File || this.data.file instanceof Blob) { + this.selectedFile = this.data.file; + this.updatePreview(); + } } } @@ -27,16 +32,20 @@ export class ImagePreviewDialogComponent implements OnInit { if (event.target.files && event.target.files[0]) { this.selectedFile = event.target.files[0]; this.updatePreview(); - this.fileInput.nativeElement.value = ''; + if (this.fileInput.nativeElement) { + this.fileInput.nativeElement.value = ''; + } } } updatePreview() { - const reader = new FileReader(); - reader.onload = () => { - this.previewUrl = reader.result; - }; - reader.readAsDataURL(this.selectedFile); + if (this.selectedFile) { + const reader = new FileReader(); + reader.onload = () => { + this.previewUrl = reader.result; + }; + reader.readAsDataURL(this.selectedFile); + } } confirm() { @@ -49,6 +58,8 @@ export class ImagePreviewDialogComponent implements OnInit { } close() { - this.dialogRef.close(); + // When closing without confirming, return undefined so the calling component knows no change was made + this.dialogRef.close(undefined); } } + From cd8f7bd882d0dad7364c9dc9ef346e30f554923e Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:13:43 -0500 Subject: [PATCH 45/48] commit --- .../certifications-add.component.html | 2 +- .../certifications-add.component.scss | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 930552c7f0..73534af572 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -12,7 +12,7 @@

Certificate Template Preview:

- Certificate Template Preview + Certificate Template Preview
diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index e69de29bb2..c2db0ec019 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -0,0 +1,16 @@ +.attachment-preview { + text-align: center; + margin-bottom: 1rem; +} + +.attachment-preview img { + max-width: 200px; + cursor: pointer; + border: 1px solid #ccc; + padding: 5px; + border-radius: 4px; +} + +.action-buttons { + margin-top: 1rem; +} From 1ddc5785dd0042be13b1c5501e3a1c4fa1695bc6 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:19:54 -0500 Subject: [PATCH 46/48] commit --- .../certifications/certifications-add.component.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.html b/src/app/manager-dashboard/certifications/certifications-add.component.html index 73534af572..000a612fcc 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.html +++ b/src/app/manager-dashboard/certifications/certifications-add.component.html @@ -10,10 +10,6 @@ {pageType, select, Add {Add} Update {Update}} Certification
-
-

Certificate Template Preview:

- Certificate Template Preview -
From 821aaef6944e6d43b64a759c717e342161cb64dd Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:21:34 -0500 Subject: [PATCH 47/48] comit --- .../certifications-add.component.scss | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss index c2db0ec019..e69de29bb2 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.scss +++ b/src/app/manager-dashboard/certifications/certifications-add.component.scss @@ -1,16 +0,0 @@ -.attachment-preview { - text-align: center; - margin-bottom: 1rem; -} - -.attachment-preview img { - max-width: 200px; - cursor: pointer; - border: 1px solid #ccc; - padding: 5px; - border-radius: 4px; -} - -.action-buttons { - margin-top: 1rem; -} From 643f5b3d924611e499d740c93c8f03baf44f5aa6 Mon Sep 17 00:00:00 2001 From: emmanuelbaa <147341761+emmanuelbaa@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:23:06 -0500 Subject: [PATCH 48/48] commit --- .../certifications/certifications-add.component.scss | 0 .../certifications/certifications-add.component.ts | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/app/manager-dashboard/certifications/certifications-add.component.scss diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.scss b/src/app/manager-dashboard/certifications/certifications-add.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/manager-dashboard/certifications/certifications-add.component.ts b/src/app/manager-dashboard/certifications/certifications-add.component.ts index b3c116921c..46f8b0c347 100644 --- a/src/app/manager-dashboard/certifications/certifications-add.component.ts +++ b/src/app/manager-dashboard/certifications/certifications-add.component.ts @@ -19,8 +19,7 @@ interface CertificationFormModel { } @Component({ - templateUrl: './certifications-add.component.html', - styleUrls: ['./certifications-add.component.scss'] + templateUrl: './certifications-add.component.html' }) export class CertificationsAddComponent implements OnInit, AfterViewChecked, CanComponentDeactivate {