feat(product): add crate and edit ops in settings
This commit is contained in:
@@ -33,7 +33,9 @@
|
||||
"edit_establishment": "edit establishment"
|
||||
},
|
||||
"product": {
|
||||
"products":"products"
|
||||
"products":"products",
|
||||
"new_product":"new product",
|
||||
"edit_product":"edit product"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
"edit_establishment": "editar establecimiento"
|
||||
},
|
||||
"product": {
|
||||
"products":"productos"
|
||||
"products":"productos",
|
||||
"new_product":"nuevo producto",
|
||||
"edit_product":"editar producto"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
"edit_establishment": "edit establishment"
|
||||
},
|
||||
"product": {
|
||||
"products":"products"
|
||||
"products":"products",
|
||||
"new_product":"new product",
|
||||
"edit_product":"edit product"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
31
src/app/components/product-add/product-add.spec.ts
Normal file
31
src/app/components/product-add/product-add.spec.ts
Normal file
@@ -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<ProductAdd>;
|
||||
|
||||
let productSettings: Partial<ProductSettings>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
31
src/app/components/product-add/product-add.ts
Normal file
31
src/app/components/product-add/product-add.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
74
src/app/components/product-edit/product-edit.spec.ts
Normal file
74
src/app/components/product-edit/product-edit.spec.ts
Normal file
@@ -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<ProductEdit>;
|
||||
|
||||
let activatedRoute: Partial<ActivatedRoute>;
|
||||
let imageStorage: Partial<ImageStorage>;
|
||||
let productSettings: Partial<ProductSettings>;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
71
src/app/components/product-edit/product-edit.ts
Normal file
71
src/app/components/product-edit/product-edit.ts
Normal file
@@ -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() {
|
||||
(<Observable<{ product: Product }>>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<app-product-add #p>
|
||||
<app-product-form [form]="p.form"></app-product-form>
|
||||
</app-product-add>
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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<ProductAddPage>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<app-product-edit #p>
|
||||
<app-product-form [form]="p.form"></app-product-form>
|
||||
</app-product-edit>
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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<ProductEditPage>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@let form = this.form();
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>{{'common.name'|translate|upperfirst}}</mat-label>
|
||||
<input matInput formControlName="name"/>
|
||||
</mat-form-field>
|
||||
<app-bar-code-input formControlName="barcode"/>
|
||||
<app-image-uploader
|
||||
appImageHandler
|
||||
[imageControl]="form.controls.image"
|
||||
[fileIn]="form.controls.image.value"
|
||||
/>
|
||||
</form>
|
||||
@@ -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<ProductForm>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
33
src/app/pages/settings/products/product-form/product-form.ts
Normal file
33
src/app/pages/settings/products/product-form/product-form.ts
Normal file
@@ -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'))
|
||||
}
|
||||
}
|
||||
11
src/app/pages/settings/products/product-formgroup.ts
Normal file
11
src/app/pages/settings/products/product-formgroup.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
export class ProductFormGroup extends FormGroup<{barcode: FormControl<string>, name: FormControl<string>, image: FormControl<File|null> }> {
|
||||
constructor(form = {
|
||||
barcode: new FormControl('', {validators: [Validators.required], nonNullable: true}),
|
||||
name: new FormControl('', {validators: [Validators.required], nonNullable: true}),
|
||||
image: new FormControl<File|null>(null),
|
||||
}) {
|
||||
super(form)
|
||||
}
|
||||
}
|
||||
@@ -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<ProductList>;
|
||||
|
||||
let activatedRoute: Partial<ActivatedRoute>;
|
||||
let router: Partial<Router>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
41
src/app/pages/settings/products/product-list/product-list.ts
Normal file
41
src/app/pages/settings/products/product-list/product-list.ts
Normal file
@@ -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']);
|
||||
}
|
||||
}
|
||||
3
src/app/pages/settings/products/products.html
Normal file
3
src/app/pages/settings/products/products.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<app-simple-layout title="settings.product.products" [withBackBtn]="true" >
|
||||
<router-outlet></router-outlet>
|
||||
</app-simple-layout>
|
||||
0
src/app/pages/settings/products/products.scss
Normal file
0
src/app/pages/settings/products/products.scss
Normal file
24
src/app/pages/settings/products/products.spec.ts
Normal file
24
src/app/pages/settings/products/products.spec.ts
Normal file
@@ -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<Products>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
13
src/app/pages/settings/products/products.ts
Normal file
13
src/app/pages/settings/products/products.ts
Normal file
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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']),
|
||||
];
|
||||
}
|
||||
|
||||
74
src/app/services/product-settings.spec.ts
Normal file
74
src/app/services/product-settings.spec.ts
Normal file
@@ -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<ImageStorage>;
|
||||
let location: Partial<Location>;
|
||||
let productDAO: Partial<ProductDAO>;
|
||||
let router: Partial<Router>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
54
src/app/services/product-settings.ts
Normal file
54
src/app/services/product-settings.ts
Normal file
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user