From 82dd3c607ab0ea61b7effdf4043776f6221e27b3 Mon Sep 17 00:00:00 2001 From: Gabriel De Los Rios Date: Sun, 22 Feb 2026 23:35:30 -0300 Subject: [PATCH] feat(product): add crate and edit ops in settings --- public/i18n/en.json | 4 +- public/i18n/es.json | 4 +- public/i18n/pt.json | 4 +- .../product-add/product-add.spec.ts | 31 ++++++++ src/app/components/product-add/product-add.ts | 31 ++++++++ .../product-edit/product-edit.spec.ts | 74 +++++++++++++++++++ .../components/product-edit/product-edit.ts | 71 ++++++++++++++++++ .../product-add-page/product-add-page.html | 3 + .../product-add-page/product-add-page.scss | 5 ++ .../product-add-page/product-add-page.spec.ts | 23 ++++++ .../product-add-page/product-add-page.ts | 13 ++++ .../product-edit-page/product-edit-page.html | 3 + .../product-edit-page/product-edit-page.scss | 5 ++ .../product-edit-page.spec.ts | 23 ++++++ .../product-edit-page/product-edit-page.ts | 13 ++++ .../products/product-form/product-form.html | 13 ++++ .../products/product-form/product-form.scss | 0 .../product-form/product-form.spec.ts | 25 +++++++ .../products/product-form/product-form.ts | 33 +++++++++ .../settings/products/product-formgroup.ts | 11 +++ .../product-list/product-list.spec.ts | 49 ++++++++++++ .../products/product-list/product-list.ts | 41 ++++++++++ src/app/pages/settings/products/products.html | 3 + src/app/pages/settings/products/products.scss | 0 .../pages/settings/products/products.spec.ts | 24 ++++++ src/app/pages/settings/products/products.ts | 13 ++++ src/app/pages/settings/settings.route.ts | 23 ++++++ src/app/pages/settings/settings.ts | 2 +- src/app/services/product-settings.spec.ts | 74 +++++++++++++++++++ src/app/services/product-settings.ts | 54 ++++++++++++++ 30 files changed, 668 insertions(+), 4 deletions(-) create mode 100644 src/app/components/product-add/product-add.spec.ts create mode 100644 src/app/components/product-add/product-add.ts create mode 100644 src/app/components/product-edit/product-edit.spec.ts create mode 100644 src/app/components/product-edit/product-edit.ts create mode 100644 src/app/pages/settings/products/product-add-page/product-add-page.html create mode 100644 src/app/pages/settings/products/product-add-page/product-add-page.scss create mode 100644 src/app/pages/settings/products/product-add-page/product-add-page.spec.ts create mode 100644 src/app/pages/settings/products/product-add-page/product-add-page.ts create mode 100644 src/app/pages/settings/products/product-edit-page/product-edit-page.html create mode 100644 src/app/pages/settings/products/product-edit-page/product-edit-page.scss create mode 100644 src/app/pages/settings/products/product-edit-page/product-edit-page.spec.ts create mode 100644 src/app/pages/settings/products/product-edit-page/product-edit-page.ts create mode 100644 src/app/pages/settings/products/product-form/product-form.html create mode 100644 src/app/pages/settings/products/product-form/product-form.scss create mode 100644 src/app/pages/settings/products/product-form/product-form.spec.ts create mode 100644 src/app/pages/settings/products/product-form/product-form.ts create mode 100644 src/app/pages/settings/products/product-formgroup.ts create mode 100644 src/app/pages/settings/products/product-list/product-list.spec.ts create mode 100644 src/app/pages/settings/products/product-list/product-list.ts create mode 100644 src/app/pages/settings/products/products.html create mode 100644 src/app/pages/settings/products/products.scss create mode 100644 src/app/pages/settings/products/products.spec.ts create mode 100644 src/app/pages/settings/products/products.ts create mode 100644 src/app/services/product-settings.spec.ts create mode 100644 src/app/services/product-settings.ts diff --git a/public/i18n/en.json b/public/i18n/en.json index ea8bc8b..2d65596 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -33,7 +33,9 @@ "edit_establishment": "edit establishment" }, "product": { - "products":"products" + "products":"products", + "new_product":"new product", + "edit_product":"edit product" } }, "common": { diff --git a/public/i18n/es.json b/public/i18n/es.json index e69cab2..a7404c5 100644 --- a/public/i18n/es.json +++ b/public/i18n/es.json @@ -33,7 +33,9 @@ "edit_establishment": "editar establecimiento" }, "product": { - "products":"productos" + "products":"productos", + "new_product":"nuevo producto", + "edit_product":"editar producto" } }, "common": { diff --git a/public/i18n/pt.json b/public/i18n/pt.json index ea8bc8b..2d65596 100644 --- a/public/i18n/pt.json +++ b/public/i18n/pt.json @@ -33,7 +33,9 @@ "edit_establishment": "edit establishment" }, "product": { - "products":"products" + "products":"products", + "new_product":"new product", + "edit_product":"edit product" } }, "common": { diff --git a/src/app/components/product-add/product-add.spec.ts b/src/app/components/product-add/product-add.spec.ts new file mode 100644 index 0000000..5a5d1ca --- /dev/null +++ b/src/app/components/product-add/product-add.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductAdd } from './product-add'; +import { ProductSettings } from '../../services/product-settings'; +import { provideTranslateService } from '@ngx-translate/core'; + +describe('ProductAdd', () => { + let component: ProductAdd; + let fixture: ComponentFixture; + + let productSettings: Partial; + + beforeEach(async () => { + productSettings = { + save: vi.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [ProductAdd], + providers: [{ provide: ProductSettings, useValue: productSettings }, provideTranslateService()], + }).compileComponents(); + + fixture = TestBed.createComponent(ProductAdd); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/product-add/product-add.ts b/src/app/components/product-add/product-add.ts new file mode 100644 index 0000000..7323300 --- /dev/null +++ b/src/app/components/product-add/product-add.ts @@ -0,0 +1,31 @@ +import { Component, inject } from '@angular/core'; +import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit'; +import { ProductFormGroup } from '../../pages/settings/products/product-formgroup'; +import { ActionBtn } from '../action-btn/action-btn'; +import { TranslatePipe } from '@ngx-translate/core'; +import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; +import { ProductSettings } from '../../services/product-settings'; +import { Product } from '../../models/Product'; + +@Component({ + selector: 'app-product-add', + imports: [ActionBtn, TranslatePipe, UpperfirstPipe], + templateUrl: './../settings-base-add-edit/settings-base-add-edit.html', + styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss', +}) +export class ProductAdd extends SettingsBaseAddEdit { + btnText = 'common.save'; + title = 'settings.product.new_product'; + readonly form = new ProductFormGroup(); + + private readonly productSettings = inject(ProductSettings); + + async submit() { + const product = new Product( + this.form.controls.barcode.value, + this.form.controls.name.value, + '', + ); + await this.productSettings.save(product, this.form.controls.image.value); + } +} diff --git a/src/app/components/product-edit/product-edit.spec.ts b/src/app/components/product-edit/product-edit.spec.ts new file mode 100644 index 0000000..bbee457 --- /dev/null +++ b/src/app/components/product-edit/product-edit.spec.ts @@ -0,0 +1,74 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductEdit } from './product-edit'; +import { ActivatedRoute } from '@angular/router'; +import { Product } from '../../models/Product'; +import { BehaviorSubject } from 'rxjs'; +import { ImageStorage } from '../../services/image-storage'; +import { ProductSettings } from '../../services/product-settings'; +import { provideTranslateService } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; + +describe('ProductEdit', () => { + let component: ProductEdit; + let fixture: ComponentFixture; + + let activatedRoute: Partial; + let imageStorage: Partial; + let productSettings: Partial; + + const dataSubject = new BehaviorSubject({ product: new Product('mock', 'mock', 'mock.jpg', 1) }); + + beforeEach(async () => { + activatedRoute = { + data: dataSubject, + }; + imageStorage = { + getImage: vi.fn().mockResolvedValue(new Blob([], { type: 'image/png' })), + }; + productSettings = { + update: vi.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [ProductEdit], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: ImageStorage, useValue: imageStorage }, + { provide: ProductSettings, useValue: productSettings }, + provideTranslateService(), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ProductEdit); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should fetch image from imageStorage', () => { + expect(imageStorage.getImage).toHaveBeenCalledTimes(1); + + }); + + //TODO: there is some sync issue by using a getter for the disabled state + it('should not call update on update btn click if form is unchanged', () => { + expect(component.disabled).toBe(true); + }); + + it('should call productSettings update on product update', async () => { + //User updates the product name + component.form.controls.name.patchValue('updated name mock'); + component.form.controls.name.markAsDirty(); + await fixture.whenStable(); + expect(component.disabled).toBe(false); + const updateBtn = fixture.debugElement.query(By.css('app-action-btn')); + //this would trigger the update anyway thats why we check for the button not to be disabled + updateBtn.triggerEventHandler('click'); + expect(productSettings.update).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/src/app/components/product-edit/product-edit.ts b/src/app/components/product-edit/product-edit.ts new file mode 100644 index 0000000..ef30dc1 --- /dev/null +++ b/src/app/components/product-edit/product-edit.ts @@ -0,0 +1,71 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { SettingsBaseAddEdit } from '../settings-base-add-edit/settings-base-add-edit'; +import { ProductFormGroup } from '../../pages/settings/products/product-formgroup'; +import { ActionBtn } from '../action-btn/action-btn'; +import { TranslatePipe } from '@ngx-translate/core'; +import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, take, tap } from 'rxjs'; +import { Product } from '../../models/Product'; +import { ImageStorage } from '../../services/image-storage'; +import { ProductSettings } from '../../services/product-settings'; + +@Component({ + selector: 'app-product-edit', + imports: [ActionBtn, TranslatePipe, UpperfirstPipe], + templateUrl: './../settings-base-add-edit/settings-base-add-edit.html', + styleUrl: './../settings-base-add-edit/settings-base-add-edit.scss', +}) +export class ProductEdit extends SettingsBaseAddEdit implements OnInit { + btnText = 'common.update'; + title = 'settings.product.edit_product'; + readonly form = new ProductFormGroup(); + private readonly activatedRoute = inject(ActivatedRoute); + private readonly imageStorage = inject(ImageStorage); + private product?: Product; + private readonly productSettings = inject(ProductSettings); + + ngOnInit() { + (>this.activatedRoute.data) + .pipe( + take(1), + tap( + (data) => + (this.product = new Product( + data.product.barcode, + data.product.name, + data.product.image, + data.product.id, + )), + ), + tap((data) => this.patchForm(data.product)), + ) + .subscribe(); + } + + async patchForm(product: Product) { + try { + this.form.controls.barcode.patchValue(product.barcode); + this.form.controls.name.patchValue(product.name); + if (product.image) { + const imgName = product.image; + const blob = await this.imageStorage.getImage(imgName); + const file = new File([blob], imgName, { type: blob.type }); + this.form.controls.image.patchValue(file); + } + } catch (e) { + console.error(e); + //TODO: reportar error + } + } + + async submit() { + if (this.product) { + const updatedBarcode = this.form.controls.barcode.value; + const updatedName = this.form.controls.name.value; + const updatedImg = this.form.controls.image.value; + const updatedProduct = new Product(updatedBarcode, updatedName, '', this.product.id); + await this.productSettings.update(this.product, updatedProduct, updatedImg); + } + } +} diff --git a/src/app/pages/settings/products/product-add-page/product-add-page.html b/src/app/pages/settings/products/product-add-page/product-add-page.html new file mode 100644 index 0000000..61f0dcb --- /dev/null +++ b/src/app/pages/settings/products/product-add-page/product-add-page.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/pages/settings/products/product-add-page/product-add-page.scss b/src/app/pages/settings/products/product-add-page/product-add-page.scss new file mode 100644 index 0000000..6a25b44 --- /dev/null +++ b/src/app/pages/settings/products/product-add-page/product-add-page.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; +} \ No newline at end of file diff --git a/src/app/pages/settings/products/product-add-page/product-add-page.spec.ts b/src/app/pages/settings/products/product-add-page/product-add-page.spec.ts new file mode 100644 index 0000000..660c7dd --- /dev/null +++ b/src/app/pages/settings/products/product-add-page/product-add-page.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductAddPage } from './product-add-page'; + +describe('ProductAddPage', () => { + let component: ProductAddPage; + let fixture: ComponentFixture; + + beforeEach(async () => { + TestBed.overrideComponent(ProductAddPage, { set: { template: `` } }); + await TestBed.configureTestingModule({ + imports: [ProductAddPage], + }).compileComponents(); + + fixture = TestBed.createComponent(ProductAddPage); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/settings/products/product-add-page/product-add-page.ts b/src/app/pages/settings/products/product-add-page/product-add-page.ts new file mode 100644 index 0000000..434885b --- /dev/null +++ b/src/app/pages/settings/products/product-add-page/product-add-page.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { ProductAdd } from "../../../../components/product-add/product-add"; +import { ProductForm } from "../product-form/product-form"; + +@Component({ + selector: 'app-product-add-page', + imports: [ProductAdd, ProductForm], + templateUrl: './product-add-page.html', + styleUrl: './product-add-page.scss', +}) +export class ProductAddPage { + +} diff --git a/src/app/pages/settings/products/product-edit-page/product-edit-page.html b/src/app/pages/settings/products/product-edit-page/product-edit-page.html new file mode 100644 index 0000000..ed619d4 --- /dev/null +++ b/src/app/pages/settings/products/product-edit-page/product-edit-page.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/pages/settings/products/product-edit-page/product-edit-page.scss b/src/app/pages/settings/products/product-edit-page/product-edit-page.scss new file mode 100644 index 0000000..6a25b44 --- /dev/null +++ b/src/app/pages/settings/products/product-edit-page/product-edit-page.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; +} \ No newline at end of file diff --git a/src/app/pages/settings/products/product-edit-page/product-edit-page.spec.ts b/src/app/pages/settings/products/product-edit-page/product-edit-page.spec.ts new file mode 100644 index 0000000..4375a75 --- /dev/null +++ b/src/app/pages/settings/products/product-edit-page/product-edit-page.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductEditPage } from './product-edit-page'; + +describe('ProductEditPage', () => { + let component: ProductEditPage; + let fixture: ComponentFixture; + + beforeEach(async () => { + TestBed.overrideComponent(ProductEditPage, { set: { template: `` } }); + await TestBed.configureTestingModule({ + imports: [ProductEditPage], + }).compileComponents(); + + fixture = TestBed.createComponent(ProductEditPage); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/settings/products/product-edit-page/product-edit-page.ts b/src/app/pages/settings/products/product-edit-page/product-edit-page.ts new file mode 100644 index 0000000..eae0edc --- /dev/null +++ b/src/app/pages/settings/products/product-edit-page/product-edit-page.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { ProductEdit } from "../../../../components/product-edit/product-edit"; +import { ProductForm } from "../product-form/product-form"; + +@Component({ + selector: 'app-product-edit-page', + imports: [ProductEdit, ProductForm], + templateUrl: './product-edit-page.html', + styleUrl: './product-edit-page.scss', +}) +export class ProductEditPage { + +} diff --git a/src/app/pages/settings/products/product-form/product-form.html b/src/app/pages/settings/products/product-form/product-form.html new file mode 100644 index 0000000..1fc88f4 --- /dev/null +++ b/src/app/pages/settings/products/product-form/product-form.html @@ -0,0 +1,13 @@ +@let form = this.form(); +
+ + {{'common.name'|translate|upperfirst}} + + + + + \ No newline at end of file diff --git a/src/app/pages/settings/products/product-form/product-form.scss b/src/app/pages/settings/products/product-form/product-form.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/settings/products/product-form/product-form.spec.ts b/src/app/pages/settings/products/product-form/product-form.spec.ts new file mode 100644 index 0000000..e3c0200 --- /dev/null +++ b/src/app/pages/settings/products/product-form/product-form.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductForm } from './product-form'; +import { provideTranslateService } from '@ngx-translate/core'; + +describe('ProductForm', () => { + let component: ProductForm; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductForm], + providers: [provideTranslateService()] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProductForm); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/settings/products/product-form/product-form.ts b/src/app/pages/settings/products/product-form/product-form.ts new file mode 100644 index 0000000..fe19297 --- /dev/null +++ b/src/app/pages/settings/products/product-form/product-form.ts @@ -0,0 +1,33 @@ +import { Component, input } from '@angular/core'; +import { ProductFormGroup } from '../product-formgroup'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { UpperfirstPipe } from '../../../../pipes/upperfirst-pipe'; +import { TranslatePipe } from '@ngx-translate/core'; +import { BarCodeInput } from '../../../../components/bar-code-input/bar-code-input'; +import { ImageUploader } from '../../../../components/image-uploader/image-uploader'; +import { ImageHandler } from '../../../../directives/image-handler'; + +@Component({ + selector: 'app-product-form', + imports: [ + MatFormField, + MatInput, + MatLabel, + ReactiveFormsModule, + TranslatePipe, + UpperfirstPipe, + BarCodeInput, + ImageUploader, + ImageHandler + ], + templateUrl: './product-form.html', + styleUrl: './product-form.scss', +}) +export class ProductForm { + form = input(new ProductFormGroup()); + constructor() { + this.form().controls.image.patchValue(new File([], 'asdf')) + } +} diff --git a/src/app/pages/settings/products/product-formgroup.ts b/src/app/pages/settings/products/product-formgroup.ts new file mode 100644 index 0000000..7779bbc --- /dev/null +++ b/src/app/pages/settings/products/product-formgroup.ts @@ -0,0 +1,11 @@ +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +export class ProductFormGroup extends FormGroup<{barcode: FormControl, name: FormControl, image: FormControl }> { + constructor(form = { + barcode: new FormControl('', {validators: [Validators.required], nonNullable: true}), + name: new FormControl('', {validators: [Validators.required], nonNullable: true}), + image: new FormControl(null), + }) { + super(form) + } +} \ No newline at end of file diff --git a/src/app/pages/settings/products/product-list/product-list.spec.ts b/src/app/pages/settings/products/product-list/product-list.spec.ts new file mode 100644 index 0000000..d79c3ca --- /dev/null +++ b/src/app/pages/settings/products/product-list/product-list.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductList } from './product-list'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { Product } from '../../../../models/Product'; +import { By } from '@angular/platform-browser'; + +describe('ProductList', () => { + let component: ProductList; + let fixture: ComponentFixture; + + let activatedRoute: Partial; + let router: Partial; + + const dataSubject = new BehaviorSubject({ products: [new Product('mock', 'mock', '')] }); + const ADD_PATH = ['settings', 'products', 'add']; + + beforeEach(async () => { + activatedRoute = { + data: dataSubject, + }; + router = { + navigate: vi.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [ProductList], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ProductList); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should navigate to add on add btn click', () => { + const addBtn = fixture.debugElement.query(By.css('app-floating-big-btn')); + addBtn.triggerEventHandler('bigClick'); + expect(router.navigate).toHaveBeenCalledExactlyOnceWith(ADD_PATH); + }); +}); diff --git a/src/app/pages/settings/products/product-list/product-list.ts b/src/app/pages/settings/products/product-list/product-list.ts new file mode 100644 index 0000000..5af7859 --- /dev/null +++ b/src/app/pages/settings/products/product-list/product-list.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { SettingsBaseList } from '../../../../components/settings-base-list/settings-base-list'; +import { map } from 'rxjs'; +import { SimpleListItem } from '../../../../components/simple-list-w-actions/SimpleListItem'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Product } from '../../../../models/Product'; +import { SimpleListItemAction } from '../../../../components/simple-list-w-actions/SimpleListItemAction'; +import { SimpleListWActions } from '../../../../components/simple-list-w-actions/simple-list-w-actions'; +import { AsyncPipe } from '@angular/common'; +import { FloatingBigBtn } from '../../../../components/floating-big-btn/floating-big-btn'; + +@Component({ + selector: 'app-product-list', + imports: [SimpleListWActions, AsyncPipe, FloatingBigBtn], + templateUrl: './../../../../components/settings-base-list/settings-base-list.html', + styleUrl: './../../../../components/settings-base-list/settings-base-list.scss', +}) +export class ProductList extends SettingsBaseList { + protected readonly activatedRoute = inject(ActivatedRoute); + private readonly router = inject(Router); + + data$ = this.activatedRoute.data.pipe( + map((data) => data['products']), + map((products: Product[]) => + products.map( + (p) => + new SimpleListItem(String(p.id), p.name ?? '', [ + new SimpleListItemAction('edit', 'edit'), + ]), + ), + ), + ); + + edit(action: { action: string; subject: string }): void { + this.router.navigate(['settings', 'products', 'edit', action.subject]); + } + + add(): void { + this.router.navigate(['settings', 'products', 'add']); + } +} diff --git a/src/app/pages/settings/products/products.html b/src/app/pages/settings/products/products.html new file mode 100644 index 0000000..a760a42 --- /dev/null +++ b/src/app/pages/settings/products/products.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/pages/settings/products/products.scss b/src/app/pages/settings/products/products.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/settings/products/products.spec.ts b/src/app/pages/settings/products/products.spec.ts new file mode 100644 index 0000000..5f6d272 --- /dev/null +++ b/src/app/pages/settings/products/products.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Products } from './products'; +import { provideTranslateService } from '@ngx-translate/core'; + +describe('Products', () => { + let component: Products; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Products], + providers: [provideTranslateService()], + }).compileComponents(); + + fixture = TestBed.createComponent(Products); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/settings/products/products.ts b/src/app/pages/settings/products/products.ts new file mode 100644 index 0000000..2048a65 --- /dev/null +++ b/src/app/pages/settings/products/products.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { SimpleLayout } from "../../../components/simple-layout/simple-layout"; +import { RouterOutlet } from "@angular/router"; + +@Component({ + selector: 'app-products', + imports: [SimpleLayout, RouterOutlet], + templateUrl: './products.html', + styleUrl: './products.scss', +}) +export class Products { + +} diff --git a/src/app/pages/settings/settings.route.ts b/src/app/pages/settings/settings.route.ts index 7a3ca53..d05f27e 100644 --- a/src/app/pages/settings/settings.route.ts +++ b/src/app/pages/settings/settings.route.ts @@ -5,6 +5,9 @@ import { chainResolver } from '../../resolvers/chain-resolver'; import { EstablishmentList } from './establishments/establishment-list/establishment-list'; import { establishmentsResolver } from '../../resolvers/establishments-resolver'; import { establishmentResolver } from '../../resolvers/establishment-resolver'; +import { ProductList } from './products/product-list/product-list'; +import { productsResolver } from '../../resolvers/products-resolver'; +import { productResolver } from '../../resolvers/product-resolver'; export const routes: Route[] = [ { @@ -55,4 +58,24 @@ export const routes: Route[] = [ } ] }, + { + path: 'products', + loadComponent: () => import('./products/products').then( c => c.Products ), + children: [ + { + path: '', + component: ProductList, + resolve: {products: productsResolver}, + }, + { + path: 'add', + loadComponent: () => import('./products/product-add-page/product-add-page').then(c => c.ProductAddPage) + }, + { + path: 'edit/:id', + loadComponent: () => import('./products/product-edit-page/product-edit-page').then(c => c.ProductEditPage), + resolve: {product: productResolver} + } + ] + }, ]; diff --git a/src/app/pages/settings/settings.ts b/src/app/pages/settings/settings.ts index 1165a0c..9e31f88 100644 --- a/src/app/pages/settings/settings.ts +++ b/src/app/pages/settings/settings.ts @@ -14,6 +14,6 @@ export class Settings { new IconNavListItem('translate', 'settings.nav.language', ['languages']), new IconNavListItem('warehouse', 'settings.nav.manage_chains', ['chains']), new IconNavListItem('store', 'settings.nav.manage_establishments', ['establishments']), - new IconNavListItem('shopping_bag', 'settings.nav.manage_products', ['/']), + new IconNavListItem('shopping_bag', 'settings.nav.manage_products', ['products']), ]; } diff --git a/src/app/services/product-settings.spec.ts b/src/app/services/product-settings.spec.ts new file mode 100644 index 0000000..ed9980d --- /dev/null +++ b/src/app/services/product-settings.spec.ts @@ -0,0 +1,74 @@ +import { TestBed } from '@angular/core/testing'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; + +import { ProductSettings } from './product-settings'; +import { ImageStorage } from './image-storage'; +import { ProductDAO } from '../dao/ProductDAO'; +import { Product } from '../models/Product'; + +const PRODUCT_SETTINGS_PATH = ['settings', 'products']; +describe('ProductSettings', () => { + let service: ProductSettings; + + let imageStorage: Partial; + let location: Partial; + let productDAO: Partial; + let router: Partial; + + let IMG_MOCK: File; + let PRODUCT_MOCK: Product; + beforeEach(() => { + IMG_MOCK = new File([], 'image.jpg', { type: 'image/jpeg' }); + PRODUCT_MOCK = new Product('mock', 'mock', 'mock.jpg', 1); + imageStorage = { + deleteImage: vi.fn(), + saveImage: vi.fn().mockResolvedValue('resolved_mock.jpg'), + }; + location = { + replaceState: vi.fn(), + }; + productDAO = { + insert: vi.fn(), + update: vi.fn(), + }; + router = { + navigate: vi.fn(), + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: ImageStorage, useValue: imageStorage }, + { provide: Location, useValue: location }, + { provide: ProductDAO, useValue: productDAO }, + { provide: Router, useValue: router }, + ], + }); + service = TestBed.inject(ProductSettings); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should save', async () => { + imageStorage.saveImage = vi.fn().mockResolvedValue(IMG_MOCK.name); + await service.save(PRODUCT_MOCK, IMG_MOCK); + expect(imageStorage.saveImage).toHaveBeenCalledExactlyOnceWith(IMG_MOCK); + expect(productDAO.insert).toHaveBeenCalledExactlyOnceWith(PRODUCT_MOCK); + expect(router.navigate).toHaveBeenCalledExactlyOnceWith(PRODUCT_SETTINGS_PATH); + }); + + it('should update', async () => { + const MODIFIED_IMG_MOCK = new File([], 'new_image.jpg', { type: 'image/jpeg' }); + const MODIFIED_PRODUCT_MOCK = {...PRODUCT_MOCK, image: ''}; + await service.update(PRODUCT_MOCK, MODIFIED_PRODUCT_MOCK, MODIFIED_IMG_MOCK); + expect(imageStorage.saveImage).toHaveBeenCalledExactlyOnceWith(MODIFIED_IMG_MOCK); + expect(imageStorage.deleteImage).toHaveBeenCalledExactlyOnceWith(PRODUCT_MOCK.image); + expect(MODIFIED_PRODUCT_MOCK.image).toEqual('resolved_mock.jpg'); + expect(productDAO.update).toHaveBeenCalledExactlyOnceWith(MODIFIED_PRODUCT_MOCK, { + id: MODIFIED_PRODUCT_MOCK.id, + }); + expect(router.navigate).toHaveBeenCalledExactlyOnceWith(PRODUCT_SETTINGS_PATH); + }); +}); diff --git a/src/app/services/product-settings.ts b/src/app/services/product-settings.ts new file mode 100644 index 0000000..580bc98 --- /dev/null +++ b/src/app/services/product-settings.ts @@ -0,0 +1,54 @@ +import { inject, Injectable } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; + +import { ImageStorage } from './image-storage'; +import { ProductDAO } from '../dao/ProductDAO'; +import { Product } from '../models/Product'; + +@Injectable({ + providedIn: 'root', +}) +export class ProductSettings { + private readonly imageStorage = inject(ImageStorage); + private readonly location = inject(Location); + private readonly productDAO = inject(ProductDAO); + private readonly router = inject(Router); + + async save(product: Product, img: File | null) { + try { + if (img) { + const imgFileName = await this.imageStorage.saveImage(img); + product.image = imgFileName; + } + await this.productDAO.insert(product); + } catch (e) { + //catch by message starts with SQLITE_CONSTRAINT_UNIQUE is duplicate + console.error(e) + //TODO: report problem + } + this.router.navigate(['settings', 'products']); + this.location.replaceState('products'); + } + + async update(prevProd: Product, updatedProd: Product, img: File | null) { + try { + let imgName = prevProd.name; + if (img) { + if (img.name !== imgName) { + imgName = await this.imageStorage.saveImage(img); + if (prevProd.image) { + this.imageStorage.deleteImage(prevProd.image); + } + updatedProd.image = imgName; + } + } + await this.productDAO.update(updatedProd, { id: updatedProd.id }); + } catch (e) { + console.error(e); + //TODO: report problem + } + this.router.navigate(['settings', 'products']); + this.location.replaceState('products'); + } +}