From d21db24ade6f808e20318e1162fd3f9bf99dcd5f Mon Sep 17 00:00:00 2001 From: tigo24 Date: Thu, 17 Sep 2026 12:30:38 +0300 Subject: [PATCH] SCRUM-387-Frontend--Rewards-Refactor-Delete-feature --- .../reward-details.component.html | 12 +- .../reward-form/reward-form.component.html | 73 ++----- .../reward-form/reward-form.component.ts | 197 ++++++------------ .../reward-list/reward-list.component.ts | 17 +- .../features/rewards/models/reward.models.ts | 46 +++- .../services/reward-details.context.ts | 27 ++- .../rewards/services/reward.service.spec.ts | 120 +++++++++-- .../rewards/services/reward.service.ts | 46 ++-- 8 files changed, 292 insertions(+), 246 deletions(-) diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html index d5b4ba6e..e0cd7821 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-details/reward-details.component.html @@ -34,23 +34,25 @@

{{ reward.name }}

{{ 'REWARD_MANAGEMENT.METRIC_REDEMPTION' | translate }} - {{ ctx.redemptionCount() }} + {{ ctx.kpiStats()?.redemptionCount ?? ctx.redemptionCount() }}
{{ 'REWARD_MANAGEMENT.METRIC_AVAILABILITY' | translate }} - {{ ctx.availableCount() }} / {{ ctx.totalCount() }} + {{ + ctx.kpiStats()?.availableStock ?? ctx.availableCount() + ' / ' + ctx.totalCount() + }}
{{ 'REWARD_MANAGEMENT.METRIC_COST' | translate }} - ${{ reward.cost }} + ${{ ctx.kpiStats()?.totalCost ?? reward.cost }}
{{ 'REWARD_MANAGEMENT.METRIC_TOP' | translate }} - {{ ctx.topRedeemed() }} + {{ ctx.kpiStats()?.topRedeemed ?? ctx.topRedeemed() }}
{{ 'REWARD_MANAGEMENT.METRIC_POINTS' | translate }} - {{ reward.pointsValue }} + {{ ctx.kpiStats()?.pointsValue ?? reward.pointsValue }}
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html index 47c03197..747540d6 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.html @@ -15,9 +15,6 @@
{{ 'REWARD_MANAGEMENT.IMAGE' | translate }} - @if (isEditMode) { - {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }} - }
{{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
- @if (isEditMode) { - - @if (isInvalid('category')) { - {{ 'REWARD_MANAGEMENT.CATEGORY_REQUIRED' | translate }} - } - } @else { - - @if (isInvalid('categoryId')) { - {{ 'REWARD_MANAGEMENT.CATEGORY_REQUIRED' | translate }} + + @if (isInvalid('categoryId')) { + {{ 'REWARD_MANAGEMENT.CATEGORY_REQUIRED' | translate }} }
- @if (isEditMode) { -
-
- {{ 'REWARD_MANAGEMENT.COST' | translate }} - {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }} -
-
- - @if (isInvalid('cost')) { - {{ 'REWARD_MANAGEMENT.COST_REQUIRED' | translate }} - } -
-
- } -
{{ 'REWARD_MANAGEMENT.PRICE' | translate }} @@ -186,26 +145,36 @@
{{ 'REWARD_MANAGEMENT.HOW_TO_REDEEM' | translate }} + {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
+ @if (isInvalid('howToRedeem')) { + {{ 'REWARD_MANAGEMENT.HOW_TO_REDEEM_REQUIRED' | translate }} + }
{{ 'REWARD_MANAGEMENT.TERMS' | translate }} + {{ 'REWARD_MANAGEMENT.REQUIRED' | translate }}
+ @if (isInvalid('termsOfUse')) { + {{ 'REWARD_MANAGEMENT.TERMS_REQUIRED' | translate }} + }
diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts index dbd2b275..a3523d5e 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-form/reward-form.component.ts @@ -3,7 +3,7 @@ import { Component, OnDestroy, OnInit, inject, input, signal } from '@angular/co import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import type { CreateRewardDto, RewardCategory, RewardItem } from '../../models/reward.models'; +import type { RewardProfileDto } from '../../models/reward.models'; import { SEEDED_REWARD_CATEGORIES } from '../../models/reward.models'; import { RewardService } from '../../services/reward.service'; import { ModalComponent } from '@app/shared/components/modal/modal.component'; @@ -28,7 +28,6 @@ export class RewardFormComponent implements OnInit, OnDestroy { readonly id = input(); isEditMode = false; - readonly categories = signal([]); readonly seededCategories = SEEDED_REWARD_CATEGORIES; readonly isSubmitting = signal(false); readonly showSuccessModal = signal(false); @@ -41,38 +40,19 @@ export class RewardFormComponent implements OnInit, OnDestroy { imageUrl: [''], name: ['', [Validators.required, Validators.minLength(2)]], description: [''], - category: [''], - categoryId: [null as number | null], - cost: [null as number | null], + categoryId: [null as number | null, [Validators.required]], price: [null as number | null, [Validators.required, Validators.min(0)]], pointsValue: [null as number | null, [Validators.required, Validators.min(1)]], - howToRedeem: [''], - termsOfUse: [''], + howToRedeem: ['', [Validators.required]], + termsOfUse: ['', [Validators.required]], }); ngOnInit(): void { const rewardId = this.id(); if (rewardId) { this.isEditMode = true; - this.form.controls.category.setValidators([Validators.required]); - this.form.controls.cost.setValidators([Validators.required, Validators.min(0)]); - this.form.controls.imageUrl.setValidators([Validators.required]); - this.form.controls.pointsValue.setValidators([Validators.required, Validators.min(0)]); this.loadReward(rewardId); - this.rewardService.getCategories().subscribe({ - next: (categories) => this.categories.set(categories), - }); - } else { - this.form.controls.categoryId.setValidators([Validators.required]); } - - this.form.controls.category.updateValueAndValidity(); - this.form.controls.categoryId.updateValueAndValidity(); - this.form.controls.cost.updateValueAndValidity(); - this.form.controls.imageUrl.updateValueAndValidity(); - this.form.controls.howToRedeem.updateValueAndValidity(); - this.form.controls.termsOfUse.updateValueAndValidity(); - this.form.controls.pointsValue.updateValueAndValidity(); } ngOnDestroy(): void { @@ -80,24 +60,28 @@ export class RewardFormComponent implements OnInit, OnDestroy { } loadReward(rewardId: string): void { - this.rewardService.getReward(rewardId).subscribe({ - next: (reward) => this.patchForm(reward), + this.rewardService.getRewardProfile(rewardId).subscribe({ + next: (response) => this.patchForm(response.profile), error: () => this.loadError.set(true), }); } - patchForm(reward: RewardItem): void { - this.imagePreview.set(reward.imageUrl); + patchForm(reward: RewardProfileDto): void { + const categoryId = + this.seededCategories.find( + (category) => category.name.toLowerCase() === reward.category.toLowerCase(), + )?.id ?? null; + + this.imagePreview.set(reward.imageUrl ?? ''); this.form.patchValue({ - imageUrl: reward.imageUrl, + imageUrl: reward.imageUrl ?? '', name: reward.name, - description: reward.description, - category: reward.category, - cost: reward.cost, - price: reward.price, - pointsValue: reward.pointsValue, - howToRedeem: reward.howToRedeem, - termsOfUse: reward.termsOfUse, + description: reward.description ?? '', + categoryId, + price: reward.monetaryValue, + pointsValue: reward.points, + howToRedeem: reward.howToRedeem ?? '', + termsOfUse: reward.termsOfUse ?? '', }); } @@ -117,30 +101,19 @@ export class RewardFormComponent implements OnInit, OnDestroy { return; } - if (!this.isEditMode) { - if (!this.isAllowedImage(file)) { - this.submitError.set(this.translate.instant('REWARD_MANAGEMENT.IMAGE_INVALID')); - inputEl.value = ''; - return; - } - this.submitError.set(null); - this.revokePreview(); - this.imageFile.set(file); - const preview = URL.createObjectURL(file); - this.imagePreview.set(preview); - this.form.patchValue({ imageUrl: preview }); - this.form.get('imageUrl')?.markAsTouched(); + if (!this.isAllowedImage(file)) { + this.submitError.set(this.translate.instant('REWARD_MANAGEMENT.IMAGE_INVALID')); + inputEl.value = ''; return; } - const reader = new FileReader(); - reader.onload = () => { - const dataUrl = String(reader.result); - this.imagePreview.set(dataUrl); - this.form.patchValue({ imageUrl: dataUrl }); - this.form.get('imageUrl')?.markAsTouched(); - }; - reader.readAsDataURL(file); + this.submitError.set(null); + this.revokePreview(); + this.imageFile.set(file); + const preview = URL.createObjectURL(file); + this.imagePreview.set(preview); + this.form.patchValue({ imageUrl: preview }); + this.form.get('imageUrl')?.markAsTouched(); } removeImage(): void { @@ -168,95 +141,49 @@ export class RewardFormComponent implements OnInit, OnDestroy { this.isSubmitting.set(true); this.submitError.set(null); - if (this.isEditMode) { - this.submitEdit(); - return; - } - - this.submitCreate(); - } - - confirmSuccess(): void { - this.showSuccessModal.set(false); - this.router.navigate(['/rewards']); - } - - private submitCreate(): void { - const value = this.form.getRawValue(); - this.rewardService - .createReward({ - name: value.name!.trim(), - description: value.description?.trim() ?? '', - categoryId: Number(value.categoryId), - points: Number(value.pointsValue), - monetaryValue: Number(value.price), - howToRedeem: value.howToRedeem?.trim() ?? '', - termsOfUse: value.termsOfUse?.trim() ?? '', - imageFile: this.imageFile(), - }) - .subscribe({ - next: () => { - this.isSubmitting.set(false); - this.showSuccessModal.set(true); - }, - error: (err: unknown) => { - this.isSubmitting.set(false); - this.submitError.set(this.extractErrorMessage(err)); - }, - }); - } - - private submitEdit(): void { const value = this.form.getRawValue(); - const dto: CreateRewardDto = { + const payload = { name: value.name!.trim(), description: value.description?.trim() ?? '', - category: value.category!, - imageUrl: value.imageUrl!, - cost: Number(value.cost), - price: Number(value.price), - pointsValue: Number(value.pointsValue), + categoryId: Number(value.categoryId), + points: Number(value.pointsValue), + monetaryValue: Number(value.price), howToRedeem: value.howToRedeem?.trim() ?? '', termsOfUse: value.termsOfUse?.trim() ?? '', - status: 'Active', - availableStock: 0, - createdAt: new Date().toISOString(), + imageFile: this.imageFile(), }; const rewardId = this.id(); - if (!rewardId) { - this.isSubmitting.set(false); - return; - } + const request$ = + this.isEditMode && rewardId + ? this.rewardService.updateReward(rewardId, payload) + : this.rewardService.createReward(payload); + + request$.subscribe({ + next: () => { + this.isSubmitting.set(false); + this.showSuccessModal.set(true); + }, + error: (err: unknown) => { + this.isSubmitting.set(false); + this.submitError.set( + this.extractErrorMessage( + err, + this.isEditMode ? 'REWARD_MANAGEMENT.UPDATE_ERROR' : 'REWARD_MANAGEMENT.CREATE_ERROR', + ), + ); + }, + }); + } - this.rewardService - .updateReward(rewardId, { - name: dto.name, - description: dto.description, - category: dto.category, - imageUrl: dto.imageUrl, - cost: dto.cost, - price: dto.price, - pointsValue: dto.pointsValue, - howToRedeem: dto.howToRedeem, - termsOfUse: dto.termsOfUse, - }) - .subscribe({ - next: () => { - this.isSubmitting.set(false); - this.showSuccessModal.set(true); - }, - error: () => { - this.isSubmitting.set(false); - this.submitError.set(this.translate.instant('REWARD_MANAGEMENT.UPDATE_ERROR')); - }, - }); + confirmSuccess(): void { + this.showSuccessModal.set(false); + this.router.navigate(['/rewards']); } private isAllowedImage(file: File): boolean { const typeOk = - ALLOWED_IMAGE_TYPES.includes(file.type) || - /\.(jpe?g|png)$/i.test(file.name); + ALLOWED_IMAGE_TYPES.includes(file.type) || /\.(jpe?g|png)$/i.test(file.name); return typeOk && file.size > 0 && file.size <= MAX_IMAGE_BYTES; } @@ -267,7 +194,7 @@ export class RewardFormComponent implements OnInit, OnDestroy { } } - private extractErrorMessage(err: unknown): string { + private extractErrorMessage(err: unknown, fallbackKey: string): string { if (err instanceof HttpErrorResponse) { if (typeof err.error?.message === 'string' && err.error.message.trim()) { return err.error.message; @@ -276,6 +203,6 @@ export class RewardFormComponent implements OnInit, OnDestroy { return err.error; } } - return this.translate.instant('REWARD_MANAGEMENT.CREATE_ERROR'); + return this.translate.instant(fallbackKey); } } diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts index 288c879a..473e7de8 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/components/reward-list/reward-list.component.ts @@ -21,6 +21,7 @@ import { } from '@app/shared/components/table/table.component'; import { ModalComponent } from '@app/shared/components/modal/modal.component'; import { ModalBodyComponent } from '@app/shared/components/modal/modal-body.component'; +import { HttpErrorResponse } from '@angular/common/http'; import type { RewardListDto, RewardListFilter, @@ -272,9 +273,9 @@ export class RewardListComponent implements AfterViewInit, OnDestroy { this.deletingReward.set(null); this.showSuccessModal.set(true); }, - error: () => { + error: (err: unknown) => { this.isDeleting.set(false); - this.deleteError.set(this.translate.instant('REWARD_MANAGEMENT.DELETE_ERROR')); + this.deleteError.set(this.extractDeleteError(err)); }, }); } @@ -294,6 +295,18 @@ export class RewardListComponent implements AfterViewInit, OnDestroy { ); } + private extractDeleteError(err: unknown): string { + if (err instanceof HttpErrorResponse) { + if (typeof err.error?.message === 'string' && err.error.message.trim()) { + return err.error.message; + } + if (typeof err.error === 'string' && err.error.trim()) { + return err.error; + } + } + return this.translate.instant('REWARD_MANAGEMENT.DELETE_ERROR'); + } + private currentFilter(): RewardListFilter { const date = this.customDate(); return { diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts index 7677b2f0..251382af 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/models/reward.models.ts @@ -63,7 +63,20 @@ export interface RewardProfileDto { readonly isActive: boolean; } -export interface CreateRewardApiInput { +export interface RewardKpiStatistics { + readonly redemptionCount: number; + readonly availableStock: string; + readonly totalCost: number; + readonly topRedeemed: number; + readonly pointsValue: number; +} + +export interface RewardProfileResponseDto { + readonly profile: RewardProfileDto; + readonly kpiStats: RewardKpiStatistics; +} + +export interface RewardWriteInput { readonly name: string; readonly description: string; readonly categoryId: number; @@ -74,6 +87,8 @@ export interface CreateRewardApiInput { readonly imageFile?: File | null; } +export type CreateRewardApiInput = RewardWriteInput; + export interface RewardItem { id: string; name: string; @@ -90,6 +105,35 @@ export interface RewardItem { createdAt: string; } +export function mapRewardProfileToItem( + profile: RewardProfileDto, + kpi?: RewardKpiStatistics | null, +): RewardItem { + return { + id: String(profile.id), + name: profile.name, + description: profile.description ?? '', + category: profile.category, + imageUrl: profile.imageUrl ?? '', + cost: kpi?.totalCost ?? 0, + price: profile.monetaryValue, + pointsValue: profile.points, + howToRedeem: profile.howToRedeem, + termsOfUse: profile.termsOfUse, + status: profile.isActive ? 'Active' : 'Inactive', + availableStock: firstStockCount(kpi?.availableStock), + createdAt: '', + }; +} + +function firstStockCount(availableStock?: string): number { + if (!availableStock) { + return 0; + } + const value = Number(availableStock.split('/')[0]); + return Number.isFinite(value) ? value : 0; +} + export type CreateRewardDto = Omit; export interface RewardRedemption { diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts index 296d9f71..f0c558dc 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward-details.context.ts @@ -1,11 +1,13 @@ import { Injectable, computed, inject, signal } from '@angular/core'; -import { forkJoin } from 'rxjs'; +import { catchError, forkJoin, of } from 'rxjs'; import type { EmployeeName, RewardInventoryItem, RewardItem, + RewardKpiStatistics, RewardRedemption, } from '../models/reward.models'; +import { mapRewardProfileToItem } from '../models/reward.models'; import { RewardService } from './reward.service'; @Injectable({ providedIn: 'root' }) @@ -14,6 +16,7 @@ export class RewardDetailsContext { readonly rewardId = signal(''); readonly reward = signal(null); + readonly kpiStats = signal(null); readonly inventory = signal([]); readonly redemptions = signal([]); readonly employees = signal([]); @@ -49,13 +52,14 @@ export class RewardDetailsContext { this.loadError.set(false); forkJoin({ - reward: this.rewardService.getReward(rewardId), - inventory: this.rewardService.getInventory(rewardId), - redemptions: this.rewardService.getRedemptions(), - employees: this.rewardService.getEmployees(), + details: this.rewardService.getRewardProfile(rewardId), + inventory: this.rewardService.getInventory(rewardId).pipe(catchError(() => of([]))), + redemptions: this.rewardService.getRedemptions().pipe(catchError(() => of([]))), + employees: this.rewardService.getEmployees().pipe(catchError(() => of([]))), }).subscribe({ - next: ({ reward, inventory, redemptions, employees }) => { - this.reward.set(reward); + next: ({ details, inventory, redemptions, employees }) => { + this.reward.set(mapRewardProfileToItem(details.profile, details.kpiStats)); + this.kpiStats.set(details.kpiStats); this.inventory.set(inventory); this.redemptions.set( redemptions.filter((item) => String(item.rewardItemId) === String(rewardId)), @@ -87,13 +91,8 @@ export class RewardDetailsContext { } this.toggling.set(true); const status = reward.status === 'Active' ? 'Inactive' : 'Active'; - this.rewardService.updateReward(reward.id, { status }).subscribe({ - next: (updated) => { - this.reward.set({ ...reward, ...updated, status }); - this.toggling.set(false); - }, - error: () => this.toggling.set(false), - }); + this.reward.set({ ...reward, status }); + this.toggling.set(false); } private redemptionDates(): string[] { diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts index 1c3a4c69..ed11a268 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.spec.ts @@ -6,7 +6,6 @@ import { environment } from '../../../../environments/environment'; import { RewardService } from './reward.service'; const REWARDS_URL = `${environment.baseUrl}/rewards`; -const MOCK_ITEMS_URL = `${environment.jsonServerUrl}/rewardItems`; describe('RewardService', () => { let service: RewardService; @@ -131,25 +130,112 @@ describe('RewardService', () => { ); }); - it('should keep get-by-id on json-server', () => { - service.getReward('8').subscribe(); + it('should PUT update as multipart FormData to /rewards/{id}', () => { + const file = new File(['img'], 'banner.png', { type: 'image/png' }); - const req = httpMock.expectOne(`${MOCK_ITEMS_URL}/8`); - expect(req.request.method).toBe('GET'); + service + .updateReward(12, { + name: 'Amazon Card', + description: 'Gift', + categoryId: 1, + points: 200, + monetaryValue: 50, + howToRedeem: 'Show code', + termsOfUse: 'No cash', + imageFile: file, + }) + .subscribe((result) => { + expect(result.id).toBe(12); + }); + + const req = httpMock.expectOne(`${REWARDS_URL}/12`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body instanceof FormData).toBe(true); + const body = req.request.body as FormData; + expect(body.get('name')).toBe('Amazon Card'); + expect(body.get('categoryId')).toBe('1'); + expect(body.get('imageFile')).toBeTruthy(); req.flush({ - id: '8', - name: 'Mock', - description: '', + id: 12, + name: 'Amazon Card', + description: 'Gift', + imageUrl: null, category: 'Gift Cards', - imageUrl: '', - cost: 1, - price: 1, - pointsValue: 1, - howToRedeem: '', - termsOfUse: '', - status: 'Active', - availableStock: 0, - createdAt: '2026-01-01T00:00:00Z', + points: 200, + monetaryValue: 50, + howToRedeem: 'Show code', + termsOfUse: 'No cash', + isActive: true, + }); + }); + + it('should DELETE a reward by id on the real API', () => { + service.deleteReward(12).subscribe(); + + const req = httpMock.expectOne(`${REWARDS_URL}/12`); + expect(req.request.method).toBe('DELETE'); + req.flush(null, { status: 204, statusText: 'No Content' }); + }); + + it('should GET reward profile by id from the real API', () => { + service.getRewardProfile(12).subscribe((response) => { + expect(response.profile.name).toBe('Amazon Card'); + }); + + const req = httpMock.expectOne(`${REWARDS_URL}/12`); + expect(req.request.method).toBe('GET'); + req.flush({ + profile: { + id: 12, + name: 'Amazon Card', + description: 'Gift', + imageUrl: null, + category: 'Gift Cards', + points: 200, + monetaryValue: 50, + howToRedeem: 'Show code', + termsOfUse: 'No cash', + isActive: true, + }, + kpiStats: { + redemptionCount: 0, + availableStock: '0/0', + totalCost: 0, + topRedeemed: 0, + pointsValue: 200, + }, + }); + }); + + it('should map GET reward by id from the real API profile', () => { + service.getReward('8').subscribe((reward) => { + expect(reward.id).toBe('8'); + expect(reward.name).toBe('Mock'); + expect(reward.status).toBe('Active'); + }); + + const req = httpMock.expectOne(`${REWARDS_URL}/8`); + expect(req.request.method).toBe('GET'); + req.flush({ + profile: { + id: 8, + name: 'Mock', + description: '', + imageUrl: null, + category: 'Gift Cards', + points: 1, + monetaryValue: 1, + howToRedeem: '', + termsOfUse: '', + isActive: true, + }, + kpiStats: { + redemptionCount: 0, + availableStock: '0/0', + totalCost: 0, + topRedeemed: 0, + pointsValue: 1, + }, }); }); }); diff --git a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts index aa8d6226..103c7cb8 100644 --- a/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts +++ b/src/Buy2.Frontend/Front/src/app/features/rewards/services/reward.service.ts @@ -3,18 +3,19 @@ import { Injectable, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { environment } from '../../../../environments/environment'; -import type { - CreateInventoryDto, - CreateRewardApiInput, - CreateRewardDto, - EmployeeName, - PaginatedRewards, - RewardCategory, - RewardInventoryItem, - RewardItem, - RewardListFilter, - RewardProfileDto, - RewardRedemption, +import { + mapRewardProfileToItem, + type CreateInventoryDto, + type EmployeeName, + type PaginatedRewards, + type RewardCategory, + type RewardInventoryItem, + type RewardItem, + type RewardListFilter, + type RewardProfileDto, + type RewardProfileResponseDto, + type RewardRedemption, + type RewardWriteInput, } from '../models/reward.models'; const REWARDS_API = `${environment.baseUrl}/rewards`; @@ -46,7 +47,7 @@ export function buildRewardListParams(filter: RewardListFilter): HttpParams { return params; } -export function buildCreateRewardFormData(input: CreateRewardApiInput): FormData { +export function buildRewardFormData(input: RewardWriteInput): FormData { const formData = new FormData(); formData.append('name', input.name); formData.append('description', input.description); @@ -66,7 +67,6 @@ export function buildCreateRewardFormData(input: CreateRewardApiInput): FormData }) export class RewardService { private readonly http = inject(HttpClient); - private readonly mockApiUrl = `${environment.jsonServerUrl}/rewardItems`; private readonly categoriesUrl = `${environment.jsonServerUrl}/rewardCategories`; private readonly redemptionsUrl = `${environment.jsonServerUrl}/rewardRedemptions`; private readonly inventoryUrl = `${environment.jsonServerUrl}/rewardInventory`; @@ -78,20 +78,26 @@ export class RewardService { }); } + getRewardProfile(id: number | string): Observable { + return this.http.get(`${REWARDS_API}/${id}`); + } + getReward(id: string): Observable { - return this.http.get(`${this.mockApiUrl}/${id}`); + return this.getRewardProfile(id).pipe( + map((response) => mapRewardProfileToItem(response.profile, response.kpiStats)), + ); } - createReward(input: CreateRewardApiInput): Observable { - return this.http.post(REWARDS_API, buildCreateRewardFormData(input)); + createReward(input: RewardWriteInput): Observable { + return this.http.post(REWARDS_API, buildRewardFormData(input)); } - updateReward(id: string, dto: Partial): Observable { - return this.http.patch(`${this.mockApiUrl}/${id}`, dto); + updateReward(id: number | string, input: RewardWriteInput): Observable { + return this.http.put(`${REWARDS_API}/${id}`, buildRewardFormData(input)); } deleteReward(id: string | number): Observable { - return this.http.delete(`${this.mockApiUrl}/${id}`); + return this.http.delete(`${REWARDS_API}/${id}`); } getCategories(): Observable {