From 80ffcca20ca971d770398398a755ae103dc244c8 Mon Sep 17 00:00:00 2001
From: trtshen <cshen_581@hotmail.com>
Date: Tue, 15 Oct 2024 13:19:30 +0800
Subject: [PATCH 1/2] [CORE-7535] achiveemnt with graphql

---
 .../app/services/achievement.service.spec.ts  |  79 +++++++++--
 .../src/app/services/achievement.service.ts   | 125 +++++++++++-------
 projects/v3/src/app/services/demo.service.ts  |   9 +-
 .../services/notifications.service.spec.ts    |  65 ++++++++-
 .../src/app/services/notifications.service.ts |   4 +-
 5 files changed, 220 insertions(+), 62 deletions(-)

diff --git a/projects/v3/src/app/services/achievement.service.spec.ts b/projects/v3/src/app/services/achievement.service.spec.ts
index cc45325f0..12ddc0422 100644
--- a/projects/v3/src/app/services/achievement.service.spec.ts
+++ b/projects/v3/src/app/services/achievement.service.spec.ts
@@ -129,15 +129,76 @@ describe('AchievementService', () => {
     });
   });
 
-  it(`should post the correct data when marking achievement as seen`, () => {
-    requestSpy.post.and.returnValue(of({}));
-    service.markAchievementAsSeen(11);
-    expect(requestSpy.post.calls.count()).toBe(1);
-    expect(requestSpy.post.calls.first().args[0].data).toEqual({
-      project_id: 1,
-      identifier: 'Achievement-11',
-      is_done: true
+  describe('graphQLGetAchievements', () => {
+    it('should return an array of achievements', (done) => {
+      const mockResponse = {
+        data: {
+          badges: [
+            {
+              id: 1,
+              name: 'Achievement 1',
+              description: 'Description 1',
+              type: 'type1',
+              badge: 'badge1',
+              openBadge: 'openBadge1',
+              points: 10,
+              isEarned: true,
+              earnedDate: '2021-01-01',
+              progress: 50,
+              active: true,
+              certificateUrl: 'url1'
+            },
+            {
+              id: 2,
+              name: 'Achievement 2',
+              description: 'Description 2',
+              type: 'type2',
+              badge: 'badge2',
+              openBadge: 'openBadge2',
+              points: 20,
+              isEarned: false,
+              earnedDate: '2021-02-01',
+              progress: 75,
+              active: false,
+              certificateUrl: 'url2'
+            }
+          ]
+        }
+      };
+
+      spyOn(service['apolloService'], 'graphQLFetch').and.returnValue(of(mockResponse));
+
+      service.graphQLGetAchievements().subscribe((achievements) => {
+        expect(achievements.length).toBe(2);
+        expect(achievements).toEqual(mockResponse.data.badges);
+        done();
+      });
+    });
+
+    it('should return an empty array if no badges are returned', (done) => {
+      const mockResponse = {
+        data: {
+          badges: []
+        }
+      };
+
+      spyOn(service['apolloService'], 'graphQLFetch').and.returnValue(of(mockResponse));
+
+      service.graphQLGetAchievements().subscribe((achievements) => {
+        expect(achievements.length).toBe(0);
+        expect(achievements).toEqual([]);
+        done();
+      });
     });
-  });
 
+    it('should handle errors gracefully', (done) => {
+      spyOn(service['apolloService'], 'graphQLFetch').and.returnValue(of({ data: null }));
+
+      service.graphQLGetAchievements().subscribe((achievements) => {
+        expect(achievements.length).toBe(0);
+        expect(achievements).toEqual([]);
+        done();
+      });
+    });
+  });
 });
diff --git a/projects/v3/src/app/services/achievement.service.ts b/projects/v3/src/app/services/achievement.service.ts
index 0e424a7d5..c522420ac 100644
--- a/projects/v3/src/app/services/achievement.service.ts
+++ b/projects/v3/src/app/services/achievement.service.ts
@@ -1,3 +1,4 @@
+import { ApolloService } from '@v3/services/apollo.service';
 import { Injectable } from '@angular/core';
 import { BehaviorSubject, Observable, of } from 'rxjs';
 import { map, shareReplay } from 'rxjs/operators';
@@ -25,12 +26,17 @@ export interface Achievement {
   image?: string;
   isEarned?: boolean;
   earnedDate?: string;
+  type: string;
+  badge: string;
+  openBadge?: string;
+  progress?: number;
+  active?: boolean;
+  certificateUrl?: string;
 }
 
 @Injectable({
   providedIn: 'root'
 })
-
 export class AchievementService {
   private _achievements$ = new BehaviorSubject<Achievement[]>([]);
   achievements$ = this._achievements$.pipe(shareReplay(1));
@@ -39,64 +45,87 @@ export class AchievementService {
   isPointsConfigured = false;
 
   constructor(
+    private apolloService: ApolloService,
     private request: RequestService,
     private utils: UtilsService,
     private demo: DemoService,
   ) { }
 
-  getAchievements(order?) {
+  graphQLGetAchievements(): Observable<Achievement[]> {
+    return this.apolloService.graphQLFetch(
+      `query badges {
+        badges {
+          id
+          name
+          description
+          type
+          badge
+          openBadge
+          points
+          isEarned
+          earnedDate
+          progress
+          active
+          certificateUrl
+        }
+      }`
+    ).pipe(map((res: {
+      data: {
+        badges: Achievement[];
+      }
+    }) => {
+      return res?.data?.badges || [];
+    }));
+  }
+
+  getAchievements() {
     if (environment.demo) {
       return setTimeout(() => this._achievements$.next(this.demo.achievements.data), 1000);
     }
-    if (!order) {
-      order = 'asc';
-    }
-    return this.request.get(api.get.achievements, {
-      params: {
-        order: order
+
+    return this.graphQLGetAchievements().pipe(map((res: Achievement[]) => {
+      const data = res;
+      if (!Array.isArray(data)) {
+        return this.request.apiResponseFormatError('Achievement format error');
       }
-    })
-      .pipe(map((res: any) => {
-        const data = res.data;
-        if (!Array.isArray(data)) {
-          return this.request.apiResponseFormatError('Achievement format error');
-        }
-        if (!data.length) {
-          this._achievements$.next([]);
-          return [];
+      if (!data.length) {
+        this._achievements$.next([]);
+        return [];
+      }
+      this.earnedPoints = 0;
+      this.isPointsConfigured = false;
+      const achievements: Achievement[] = [];
+      data.forEach(achievement => {
+        if (!this.utils.has(achievement, 'id') ||
+          !this.utils.has(achievement, 'name') ||
+          !this.utils.has(achievement, 'description') ||
+          !this.utils.has(achievement, 'badge') ||
+          !this.utils.has(achievement, 'points') ||
+          !this.utils.has(achievement, 'isEarned') ||
+          !this.utils.has(achievement, 'earnedDate')) {
+          return this.request.apiResponseFormatError('Achievement object format error');
         }
-        this.earnedPoints = 0;
-        this.isPointsConfigured = false;
-        const achievements: Array<Achievement> = [];
-        data.forEach(achievement => {
-          if (!this.utils.has(achievement, 'id') ||
-            !this.utils.has(achievement, 'name') ||
-            !this.utils.has(achievement, 'description') ||
-            !this.utils.has(achievement, 'badge') ||
-            !this.utils.has(achievement, 'points') ||
-            !this.utils.has(achievement, 'isEarned') ||
-            !this.utils.has(achievement, 'earnedDate')) {
-            return this.request.apiResponseFormatError('Achievement object format error');
-          }
-          achievements.push({
-            id: achievement.id,
-            name: achievement.name,
-            description: achievement.description,
-            points: +achievement.points,
-            image: achievement.badge,
-            isEarned: achievement.isEarned,
-            earnedDate: achievement.earnedDate,
-          });
-          if (achievement.points > 0) {
-            this.isPointsConfigured = true;
-            if (achievement.isEarned) {
-              this.earnedPoints += +achievement.points;
-            }
-          }
+        achievements.push({
+          id: achievement.id,
+          name: achievement.name,
+          description: achievement.description,
+          points: +achievement.points,
+          image: achievement.badge,
+          isEarned: achievement.isEarned,
+          earnedDate: achievement.earnedDate,
+          type: achievement.type,
+          badge: achievement.badge,
         });
-        this._achievements$.next(achievements);
-        return achievements;
-      })).subscribe();
+        if (achievement.points > 0) {
+          this.isPointsConfigured = true;
+          if (achievement.isEarned) {
+            this.earnedPoints += +achievement.points;
+          }
+        }
+      });
+      this._achievements$.next(achievements);
+      return achievements;
+    })).subscribe();
   }
 
   getEarnedPoints() {
diff --git a/projects/v3/src/app/services/demo.service.ts b/projects/v3/src/app/services/demo.service.ts
index 7fb17b592..a70a9326e 100644
--- a/projects/v3/src/app/services/demo.service.ts
+++ b/projects/v3/src/app/services/demo.service.ts
@@ -135,10 +135,15 @@ export class DemoService {
         id: i + 1,
         name: `Badge${ i + 1 }`,
         description: i < 2 ? `Badge description${ i + 1 }` : this.description,
-        image: 'https://www.filepicker.io/api/file/Pt5V84aSTvyYEil1bttc',
+        image: 'assets/images/badge.png',
         points: Math.floor(Math.random() * 1000),
         isEarned: i < 3,
-        earnedDate: '2021-10-04 05:44:49'
+        earnedDate: '2021-10-04 05:44:49',
+        progress: Math.random(),
+        active: i % 2 === 0,
+        certificateUrl: 'assets/images/badge.png',
+        type: '',
+        badge: 'assets/images/badge.png',
       }))
     };
   }
diff --git a/projects/v3/src/app/services/notifications.service.spec.ts b/projects/v3/src/app/services/notifications.service.spec.ts
index c03078a9c..b44ded893 100644
--- a/projects/v3/src/app/services/notifications.service.spec.ts
+++ b/projects/v3/src/app/services/notifications.service.spec.ts
@@ -6,7 +6,7 @@ import { AchievementService } from './achievement.service';
 import { ApolloService } from './apollo.service';
 import { EventService } from './event.service';
 
-import { NotificationsService } from './notifications.service';
+import { api, NotificationsService } from './notifications.service';
 import { BrowserStorageService } from './storage.service';
 import { UtilsService } from './utils.service';
 
@@ -81,4 +81,67 @@ describe('NotificationsService', () => {
       });
     });
   });
+
+  describe('markTodoItemAsDone', () => {
+    let requestService: jasmine.SpyObj<RequestService>;
+    let storageService: jasmine.SpyObj<BrowserStorageService>;
+
+    beforeEach(() => {
+      requestService = TestBed.inject(RequestService) as jasmine.SpyObj<RequestService>;
+      storageService = TestBed.inject(BrowserStorageService) as jasmine.SpyObj<BrowserStorageService>;
+    });
+
+    it('should call request.post with correct parameters when identifier is provided', () => {
+      const projectId = 123;
+      const identifier = 'test-identifier';
+      storageService.getUser.and.returnValue({ projectId });
+
+      service.markTodoItemAsDone({ identifier });
+
+      expect(requestService.post).toHaveBeenCalledWith({
+        endPoint: api.post.todoItem,
+        data: {
+          identifier,
+          project_id: projectId,
+          is_done: true
+        }
+      });
+    });
+
+    it('should call request.post with correct parameters when id is provided', () => {
+      const projectId = 123;
+      const id = 456;
+      storageService.getUser.and.returnValue({ projectId });
+
+      service.markTodoItemAsDone({ id });
+
+      expect(requestService.post).toHaveBeenCalledWith({
+        endPoint: api.post.todoItem,
+        data: {
+          id,
+          project_id: projectId,
+          is_done: true
+        }
+      });
+    });
+
+    it('should call request.post with correct parameters when both identifier and id are provided', () => {
+      const projectId = 123;
+      const identifier = 'test-identifier';
+      const id = 456;
+      storageService.getUser.and.returnValue({ projectId });
+
+      service.markTodoItemAsDone({ identifier, id });
+
+      expect(requestService.post).toHaveBeenCalledWith({
+        endPoint: api.post.todoItem,
+        data: {
+          identifier,
+          id,
+          project_id: projectId,
+          is_done: true
+        }
+      });
+    });
+  });
 });
diff --git a/projects/v3/src/app/services/notifications.service.ts b/projects/v3/src/app/services/notifications.service.ts
index fe19be654..db0ea4503 100644
--- a/projects/v3/src/app/services/notifications.service.ts
+++ b/projects/v3/src/app/services/notifications.service.ts
@@ -87,7 +87,7 @@ export interface TodoItem {
   timeline_id?: number;
 }
 
-const api = {
+export const api = {
   get: {
     todoItem: 'api/v2/motivations/todo_item/list.json',
     events: 'api/v2/act/event/list.json',
@@ -287,7 +287,7 @@ export class NotificationsService {
    *    description: "qwert yuiop asdfg asdff"
    * });
    */
-  async achievementPopUp(type: string, achievement: Achievement, options?) {
+  async achievementPopUp(type: string, achievement: Partial<Achievement>, options?) {
     const component = AchievementPopUpComponent;
     const componentProps = {
       type,

From 4cabec65e55b9bb588b3483a93a55068ceedc579 Mon Sep 17 00:00:00 2001
From: trtshen <cshen_581@hotmail.com>
Date: Thu, 17 Oct 2024 14:39:03 +0800
Subject: [PATCH 2/2] use achievement query

---
 .../src/app/services/achievement.service.ts   | 164 ++++++++++--------
 .../v3/src/app/services/shared.service.ts     |   4 +-
 2 files changed, 96 insertions(+), 72 deletions(-)

diff --git a/projects/v3/src/app/services/achievement.service.ts b/projects/v3/src/app/services/achievement.service.ts
index c522420ac..7c4021794 100644
--- a/projects/v3/src/app/services/achievement.service.ts
+++ b/projects/v3/src/app/services/achievement.service.ts
@@ -35,7 +35,7 @@ export interface Achievement {
 }
 
 @Injectable({
-  providedIn: 'root'
+  providedIn: "root",
 })
 export class AchievementService {
   private _achievements$ = new BehaviorSubject<Achievement[]>([]);
@@ -48,84 +48,108 @@ export class AchievementService {
     private apolloService: ApolloService,
     private request: RequestService,
     private utils: UtilsService,
-    private demo: DemoService,
-  ) { }
+    private demo: DemoService
+  ) {}
 
+  /**
+   * [graphQLGetAchievements description]
+   * @link https://intersective.github.io/core-graphql-api/query.doc.html#:~:text=achievements(filter%3A%20String%2C%20type%3A%20String%2C%20active%3A%20Boolean)%3A%20%5BAchievement%5D
+   * @return  {Observable<Achievement>[]}          achievement list in badges
+   */
   graphQLGetAchievements(): Observable<Achievement[]> {
-    return this.apolloService.graphQLFetch(
-      `query badges {
-        badges {
-          id
-          name
-          description
-          type
-          badge
-          openBadge
-          points
-          isEarned
-          earnedDate
-          progress
-          active
-          certificateUrl
-        }
-      }`
-    ).pipe(map((res: {
-      data: {
-        badges: Achievement[];
-      }
-    }) => {
-      return res?.data?.badges || [];
-    }));
+    return this.apolloService
+      .graphQLFetch(
+        `query achievements {
+          achievements {
+            id
+            name
+            description
+            type
+            badge
+            openBadge
+            points
+            isEarned
+            earnedDate
+            progress
+            active
+            certificateUrl
+          }
+        }`
+      )
+      .pipe(
+        map(
+          (res: {
+            data: {
+              achievements: Achievement[];
+            };
+          }) => {
+            return res?.data?.achievements || [];
+          }
+        )
+      );
   }
 
   getAchievements() {
     if (environment.demo) {
-      return setTimeout(() => this._achievements$.next(this.demo.achievements.data), 1000);
+      return setTimeout(
+        () => this._achievements$.next(this.demo.achievements.data),
+        1000
+      );
     }
 
-    return this.graphQLGetAchievements().pipe(map((res: Achievement[]) => {
-      const data = res;
-      if (!Array.isArray(data)) {
-        return this.request.apiResponseFormatError('Achievement format error');
-      }
-      if (!data.length) {
-        this._achievements$.next([]);
-        return [];
-      }
-      this.earnedPoints = 0;
-      this.isPointsConfigured = false;
-      const achievements: Achievement[] = [];
-      data.forEach(achievement => {
-        if (!this.utils.has(achievement, 'id') ||
-          !this.utils.has(achievement, 'name') ||
-          !this.utils.has(achievement, 'description') ||
-          !this.utils.has(achievement, 'badge') ||
-          !this.utils.has(achievement, 'points') ||
-          !this.utils.has(achievement, 'isEarned') ||
-          !this.utils.has(achievement, 'earnedDate')) {
-          return this.request.apiResponseFormatError('Achievement object format error');
-        }
-        achievements.push({
-          id: achievement.id,
-          name: achievement.name,
-          description: achievement.description,
-          points: +achievement.points,
-          image: achievement.badge,
-          isEarned: achievement.isEarned,
-          earnedDate: achievement.earnedDate,
-          type: achievement.type,
-          badge: achievement.badge,
-        });
-        if (achievement.points > 0) {
-          this.isPointsConfigured = true;
-          if (achievement.isEarned) {
-            this.earnedPoints += +achievement.points;
+    return this.graphQLGetAchievements()
+      .pipe(
+        map((res: Achievement[]) => {
+          const data = res;
+          if (!Array.isArray(data)) {
+            return this.request.apiResponseFormatError(
+              "Achievement format error"
+            );
+          }
+          if (!data.length) {
+            this._achievements$.next([]);
+            return [];
           }
-        }
-      });
-      this._achievements$.next(achievements);
-      return achievements;
-    })).subscribe();
+          this.earnedPoints = 0;
+          this.isPointsConfigured = false;
+          const achievements: Achievement[] = [];
+          data.forEach((achievement) => {
+            if (
+              !this.utils.has(achievement, "id") ||
+              !this.utils.has(achievement, "name") ||
+              !this.utils.has(achievement, "description") ||
+              !this.utils.has(achievement, "badge") ||
+              !this.utils.has(achievement, "points") ||
+              !this.utils.has(achievement, "isEarned") ||
+              !this.utils.has(achievement, "earnedDate")
+            ) {
+              return this.request.apiResponseFormatError(
+                "Achievement object format error"
+              );
+            }
+            achievements.push({
+              id: achievement.id,
+              name: achievement.name,
+              description: achievement.description,
+              points: +achievement.points,
+              image: achievement.badge,
+              isEarned: achievement.isEarned,
+              earnedDate: achievement.earnedDate,
+              type: achievement.type,
+              badge: achievement.badge,
+            });
+            if (achievement.points > 0) {
+              this.isPointsConfigured = true;
+              if (achievement.isEarned) {
+                this.earnedPoints += +achievement.points;
+              }
+            }
+          });
+          this._achievements$.next(achievements);
+          return achievements;
+        })
+      )
+      .subscribe();
   }
 
   getEarnedPoints() {
diff --git a/projects/v3/src/app/services/shared.service.ts b/projects/v3/src/app/services/shared.service.ts
index 13fde4f5e..e89b75c18 100644
--- a/projects/v3/src/app/services/shared.service.ts
+++ b/projects/v3/src/app/services/shared.service.ts
@@ -91,8 +91,8 @@ export class SharedService {
       `query user {
         user {
           teams {
-              id
-              name
+            id
+            name
           }
         }
       }`
