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:
+
+
-
+
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:
-
-
-
+
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
+
\ 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 @@
0" [isForm]="true" [includeIds]="courseIds">
-
\ 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:
+
+
-
+
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:
-
-
-
+
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