From bcb158b837212d87f40a9cae3070ae02c554a689 Mon Sep 17 00:00:00 2001 From: Amritanshu Date: Sun, 16 Jun 2019 17:45:16 +0530 Subject: [PATCH] Product Error on *ngIf, new a dealbreaker, but not sure. --- barker/models/master.py | 232 +++++++++++------- barker/routes.py | 4 - barker/views/product.py | 149 +++++++---- bookie/src/app/app-routing.module.ts | 2 +- bookie/src/app/home/home.component.html | 3 + .../product-group-list-datasource.ts | 12 +- .../product-group-list.component.ts | 29 ++- .../product-detail.component.html | 36 ++- .../product-detail.component.ts | 37 +-- .../product-list-resolver.service.spec.ts | 8 +- .../product-list/product-list-datasource.ts | 47 ++-- .../product-list/product-list.component.html | 24 +- .../product-list/product-list.component.ts | 67 +++-- .../product/product-resolver.service.spec.ts | 8 +- .../product/product-routing.module.spec.ts | 13 - .../src/app/product/product-routing.module.ts | 66 ----- bookie/src/app/product/product.module.spec.ts | 13 - bookie/src/app/product/product.service.ts | 19 +- .../product/products-routing.module.spec.ts | 13 + .../app/product/products-routing.module.ts | 70 ++++++ .../src/app/product/products.module.spec.ts | 13 + .../{product.module.ts => products.module.ts} | 14 +- 22 files changed, 513 insertions(+), 366 deletions(-) delete mode 100644 bookie/src/app/product/product-routing.module.spec.ts delete mode 100644 bookie/src/app/product/product-routing.module.ts delete mode 100644 bookie/src/app/product/product.module.spec.ts create mode 100644 bookie/src/app/product/products-routing.module.spec.ts create mode 100644 bookie/src/app/product/products-routing.module.ts create mode 100644 bookie/src/app/product/products.module.spec.ts rename bookie/src/app/product/{product.module.ts => products.module.ts} (78%) diff --git a/barker/models/master.py b/barker/models/master.py index 6c509f8..bd7059f 100644 --- a/barker/models/master.py +++ b/barker/models/master.py @@ -7,7 +7,10 @@ from sqlalchemy import ( Numeric, Boolean, ForeignKey, - Integer, case, JSON) + Integer, + case, + JSON, +) from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship from .meta import Base @@ -15,13 +18,13 @@ from barker.models.guidtype import GUID class Customer(Base): - __tablename__ = 'customers' + __tablename__ = "customers" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - company = Column('company', Unicode(255), nullable=False) - name = Column('name', Unicode(255), nullable=False) - phone = Column('phone', Unicode(255), nullable=False, unique=True) - address = Column('address', Unicode(255), nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + company = Column("company", Unicode(255), nullable=False) + name = Column("name", Unicode(255), nullable=False) + phone = Column("phone", Unicode(255), nullable=False, unique=True) + address = Column("address", Unicode(255), nullable=False) @property def __name__(self): @@ -36,18 +39,18 @@ class Customer(Base): @classmethod def cash(cls): - return uuid.UUID('2c716f4b-0736-429a-ad51-610d7c47cb5e') + return uuid.UUID("2c716f4b-0736-429a-ad51-610d7c47cb5e") class FoodTable(Base): - __tablename__ = 'food_tables' + __tablename__ = "food_tables" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), nullable=False, unique=True) - location = Column('location', Unicode(255), nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), nullable=False, unique=True) + location = Column("location", Unicode(255), nullable=False) - is_active = Column('is_active', Boolean, nullable=False) - sort_order = Column('sort_order', Numeric, nullable=False) + is_active = Column("is_active", Boolean, nullable=False) + sort_order = Column("sort_order", Numeric, nullable=False) @property def __name__(self): @@ -62,11 +65,11 @@ class FoodTable(Base): class Tax(Base): - __tablename__ = 'taxes' + __tablename__ = "taxes" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), nullable=False, unique=True) - rate = Column('rate', Numeric, nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), nullable=False, unique=True) + rate = Column("rate", Numeric, nullable=False) is_fixture = Column("is_fixture", Boolean, nullable=False) def __init__(self, name=None, rate=None, is_fixture=False, id=None): @@ -77,19 +80,29 @@ class Tax(Base): class ProductGroup(Base): - __tablename__ = 'product_groups' + __tablename__ = "product_groups" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), nullable=False, unique=True) - discount_limit = Column('discount_limit', Numeric, nullable=False) - is_modifier_compulsory = Column('is_modifier_compulsory', Boolean, nullable=False) - group_type = Column('group_type', Unicode(255), nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), nullable=False, unique=True) + discount_limit = Column("discount_limit", Numeric, nullable=False) + is_modifier_compulsory = Column("is_modifier_compulsory", Boolean, nullable=False) + group_type = Column("group_type", Unicode(255), nullable=False) - is_active = Column('is_active', Boolean, nullable=False) - is_fixture = Column('is_fixture', Boolean, nullable=False) - sort_order = Column('sort_order', Numeric, nullable=False) + is_active = Column("is_active", Boolean, nullable=False) + is_fixture = Column("is_fixture", Boolean, nullable=False) + sort_order = Column("sort_order", Numeric, nullable=False) - def __init__(self, name, discount_limit, is_modifier_compulsory, group_type, is_active, sort_order, is_fixture=False, id=None): + def __init__( + self, + name, + discount_limit, + is_modifier_compulsory, + group_type, + is_active, + sort_order, + is_fixture=False, + id=None, + ): self.name = name self.discount_limit = discount_limit self.is_modifier_compulsory = is_modifier_compulsory @@ -101,27 +114,41 @@ class ProductGroup(Base): class Product(Base): - __tablename__ = 'products' - __table_args__ = (UniqueConstraint('name', 'units'),) + __tablename__ = "products" + __table_args__ = (UniqueConstraint("name", "units"),) - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), nullable=False) - units = Column('units', Unicode(255), nullable=False) - product_group_id = Column('product_group_id', GUID(), ForeignKey('product_groups.id'), nullable=False) - tax_id = Column('tax_id', GUID(), ForeignKey('taxes.id'), nullable=False) - price = Column('price', Numeric, nullable=False) - has_happy_hour = Column('has_happy_hour', Boolean, nullable=False) - is_not_available = Column('is_not_available', Boolean, nullable=False) - quantity = Column('quantity', Numeric, nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), nullable=False) + units = Column("units", Unicode(255), nullable=False) + product_group_id = Column( + "product_group_id", GUID(), ForeignKey("product_groups.id"), nullable=False + ) + tax_id = Column("tax_id", GUID(), ForeignKey("taxes.id"), nullable=False) + price = Column("price", Numeric, nullable=False) + has_happy_hour = Column("has_happy_hour", Boolean, nullable=False) + is_not_available = Column("is_not_available", Boolean, nullable=False) + quantity = Column("quantity", Numeric, nullable=False) - is_active = Column('is_active', Boolean, nullable=False) - sort_order = Column('sort_order', Numeric, nullable=False) + is_active = Column("is_active", Boolean, nullable=False) + sort_order = Column("sort_order", Numeric, nullable=False) - product_group = relationship('ProductGroup', backref='products') - tax = relationship('Tax', foreign_keys=tax_id) + product_group = relationship("ProductGroup", backref="products") + tax = relationship("Tax", foreign_keys=tax_id) - def __init__(self, name=None, units=None, product_group_id=None, tax_id=None, price=None, - has_happy_hour=None, is_not_available=None, quantity=None, is_active=None, sort_order=None, id=None): + def __init__( + self, + name=None, + units=None, + product_group_id=None, + tax_id=None, + price=None, + has_happy_hour=None, + is_not_available=None, + quantity=None, + is_active=None, + sort_order=0, + id=None, + ): self.name = name self.units = units self.product_group_id = product_group_id @@ -140,25 +167,28 @@ class Product(Base): @full_name.expression def full_name(cls): - return cls.name + case([(cls.units != '', ' (' + cls.units + ')')], else_='') + return cls.name + case([(cls.units != "", " (" + cls.units + ")")], else_="") def can_delete(self, advanced_delete): if self.is_fixture: - return False, "{0} is a fixture and cannot be edited or deleted.".format(self.name) + return ( + False, + "{0} is a fixture and cannot be edited or deleted.".format(self.name), + ) if self.is_active: - return False, 'Product is active' + return False, "Product is active" if len(self.inventories) > 0 and not advanced_delete: - return False, 'Product has entries' - return True, '' + return False, "Product has entries" + return True, "" class Modifier(Base): - __tablename__ = 'modifiers' + __tablename__ = "modifiers" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), nullable=False, unique=True) - show_in_bill = Column('show_in_bill', Boolean, nullable=False) - price = Column('price', Numeric, nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), nullable=False, unique=True) + show_in_bill = Column("show_in_bill", Boolean, nullable=False) + price = Column("price", Numeric, nullable=False) def __init__(self, name=None, show_in_bill=None, price=None, id=None): self.id = id @@ -168,17 +198,23 @@ class Modifier(Base): class ProductGroupModifier(Base): - __tablename__ = 'product_group_modifiers' + __tablename__ = "product_group_modifiers" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - product_group_id = Column('product_group_id', GUID(), ForeignKey('product_groups.id')) - modifier_id = Column('modifier_id', GUID(), ForeignKey('modifiers.id'), nullable=False) - show_automatically = Column('show_automatically', Boolean, nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + product_group_id = Column( + "product_group_id", GUID(), ForeignKey("product_groups.id") + ) + modifier_id = Column( + "modifier_id", GUID(), ForeignKey("modifiers.id"), nullable=False + ) + show_automatically = Column("show_automatically", Boolean, nullable=False) - product_group = relationship('ProductGroup', backref='modifiers') - modifier = relationship('Modifier', backref='product_groups') + product_group = relationship("ProductGroup", backref="modifiers") + modifier = relationship("Modifier", backref="product_groups") - def __init__(self, product_group_id=None, modifier_id=None, show_automatically=None, id=None): + def __init__( + self, product_group_id=None, modifier_id=None, show_automatically=None, id=None + ): self.id = id self.product_group_id = product_group_id self.modifier_id = modifier_id @@ -186,11 +222,11 @@ class ProductGroupModifier(Base): class DbSetting(Base): - __tablename__ = 'settings' + __tablename__ = "settings" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), unique=True, nullable=False) - data = Column('data', JSON) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), unique=True, nullable=False) + data = Column("data", JSON) def __init__(self, id=None, name=None, data=None): self.id = id @@ -199,10 +235,10 @@ class DbSetting(Base): class Location(Base): - __tablename__ = 'locations' + __tablename__ = "locations" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), unique=True, nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), unique=True, nullable=False) def __init__(self, id=None, name=None): self.id = id @@ -210,13 +246,15 @@ class Location(Base): class MachineLocation(Base): - __tablename__ = 'machine_locations' + __tablename__ = "machine_locations" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - machine_name = Column('machine_name', Unicode(255), unique=True, nullable=False) - location_id = Column('location_id', GUID(), ForeignKey('locations.id'), nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + machine_name = Column("machine_name", Unicode(255), unique=True, nullable=False) + location_id = Column( + "location_id", GUID(), ForeignKey("locations.id"), nullable=False + ) - location = relationship('Location', backref='machines') + location = relationship("Location", backref="machines") def __init__(self, machine_name=None, location_id=None, id=None): self.machine_name = machine_name @@ -225,11 +263,11 @@ class MachineLocation(Base): class Printer(Base): - __tablename__ = 'printers' + __tablename__ = "printers" - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - name = Column('name', Unicode(255), unique=True, nullable=False) - cut_code = Column('cut_code', Unicode(255), nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + name = Column("name", Unicode(255), unique=True, nullable=False) + cut_code = Column("cut_code", Unicode(255), nullable=False) def __init__(self, id=None, name=None, cut_code=None): self.id = id @@ -238,18 +276,22 @@ class Printer(Base): class PrintLocation(Base): - __tablename__ = 'print_locations' - __table_args__ = (UniqueConstraint('product_group_id', 'location_id'),) + __tablename__ = "print_locations" + __table_args__ = (UniqueConstraint("product_group_id", "location_id"),) - id = Column('id', GUID(), primary_key=True, default=uuid.uuid4) - product_group_id = Column('product_group_id', GUID(), ForeignKey('product_groups.id')) - location_id = Column('location_id', GUID(), ForeignKey('locations.id'), nullable=False) - printer_id = Column('printer_id', GUID(), ForeignKey('printers.id'), nullable=False) - copies = Column('copies', Numeric, nullable=False) + id = Column("id", GUID(), primary_key=True, default=uuid.uuid4) + product_group_id = Column( + "product_group_id", GUID(), ForeignKey("product_groups.id") + ) + location_id = Column( + "location_id", GUID(), ForeignKey("locations.id"), nullable=False + ) + printer_id = Column("printer_id", GUID(), ForeignKey("printers.id"), nullable=False) + copies = Column("copies", Numeric, nullable=False) - product_group = relationship('ProductGroup', backref='print_locations') - location = relationship('Location', backref='print_locations') - printer = relationship('Printer', backref='print_locations') + product_group = relationship("ProductGroup", backref="print_locations") + location = relationship("Location", backref="print_locations") + printer = relationship("Printer", backref="print_locations") def __init__(self, product_group_id, location_id, printer_id, copies): self.product_group_id = product_group_id @@ -259,13 +301,13 @@ class PrintLocation(Base): class SettleOption(Base): - __tablename__ = 'settle_options' + __tablename__ = "settle_options" - id = Column('id', Integer, primary_key=True) - name = Column('name', Unicode(255), unique=True, nullable=False) - show_in_choices = Column('show_in_choices', Boolean, nullable=False) - group = Column('display_group', Integer, nullable=False) - is_print = Column('is_print', Boolean, nullable=False) + id = Column("id", Integer, primary_key=True) + name = Column("name", Unicode(255), unique=True, nullable=False) + show_in_choices = Column("show_in_choices", Boolean, nullable=False) + group = Column("display_group", Integer, nullable=False) + is_print = Column("is_print", Boolean, nullable=False) def __init__(self, name, show_in_choices, group, is_print, id): self.id = id diff --git a/barker/routes.py b/barker/routes.py index fefcaf6..52e9c67 100644 --- a/barker/routes.py +++ b/barker/routes.py @@ -55,10 +55,6 @@ def includeme(config): config.add_route("v1_product_groups_list", "/v1/product-groups") config.add_route("v1_product_group_types_list", "/v1/product-group-types") - config.add_route('product_group', '/ProductGroup.json') - config.add_route('product_group_list', '/ProductGroups.json') - config.add_route('product_group_id', '/ProductGroup/{id}.json') - config.add_route('product_group_type_list', '/ProductGroupTypes.json') config.add_route('quantity_sold', '/QuantitySold.json') diff --git a/barker/views/product.py b/barker/views/product.py index 1fbc66a..d7ba579 100644 --- a/barker/views/product.py +++ b/barker/views/product.py @@ -9,7 +9,13 @@ from barker.models import Product from barker.models.validation_exception import ValidationError -@view_config(request_method='PUT', route_name='v1_products_new', renderer='json', permission='Products', trans=True) +@view_config( + request_method="POST", + route_name="v1_products_new", + renderer="json", + permission="Products", + trans=True, +) def save(request): json = request.json_body @@ -57,15 +63,20 @@ def save(request): has_happy_hour, is_not_available, quantity, - is_active + is_active, ) request.dbsession.add(item) transaction.commit() - item = request.dbsession.query(Product).filter(Product.id == item.id).first() - return product_info(item) + return product_info(item.id, request.dbsession) -@view_config(request_method='POST', route_name='v1_products_id', renderer='json', permission='Products', trans=True) +@view_config( + request_method="PUT", + route_name="v1_products_id", + renderer="json", + permission="Products", + trans=True, +) def update(request): item = ( request.dbsession.query(Product) @@ -76,30 +87,39 @@ def update(request): item.name = json["name"].strip() item.units = json["units"].strip() item.product_group_id = uuid.UUID(json["productGroup"]["id"]) - item.vat_id = uuid.UUID(json["tax"]["id"]) + item.tax_id = uuid.UUID(json["tax"]["id"]) try: item.price = Decimal(json["price"]) if item.price < 0: raise ValidationError("Price must be a decimal >= 0") except (ValueError, InvalidOperation): raise ValidationError("Price must be a decimal >= 0") - item.has_happy_hour = json['hasHappyHour'] - item.is_not_available = json['isNotAvailable'] + item.has_happy_hour = json["hasHappyHour"] + item.is_not_available = json["isNotAvailable"] try: item.quantity = Decimal(json["quantity"]) if item.price < 0: raise ValidationError("Quantity must be a decimal >= 0") except (ValueError, InvalidOperation): raise ValidationError("Quantity must be a decimal >= 0") - item.is_active = json['isActive'] + item.is_active = json["isActive"] transaction.commit() - item = request.dbsession.query(Product).filter(Product.id == item.id).first() - return product_info(item) + return product_info(item.id, request.dbsession) -@view_config(request_method='DELETE', route_name='v1_products_id', renderer='json', permission='Products', trans=True) +@view_config( + request_method="DELETE", + route_name="v1_products_id", + renderer="json", + permission="Products", + trans=True, +) def delete(request): - item = request.dbsession.query(Product).filter(Product.id == uuid.UUID(request.matchdict['id'])).first() + item = ( + request.dbsession.query(Product) + .filter(Product.id == uuid.UUID(request.matchdict["id"])) + .first() + ) if item is None: response = Response("Product not Found") response.status_int = 500 @@ -110,56 +130,97 @@ def delete(request): return response -@view_config(request_method='GET', route_name='v1_products_id', renderer='json', permission='Authenticated') +@view_config( + request_method="GET", + route_name="v1_products_new", + renderer="json", + permission="Authenticated", +) +def show_blank(request): + return product_info(None, request.dbsession) + + +@view_config( + request_method="GET", + route_name="v1_products_id", + renderer="json", + permission="Authenticated", +) def show_id(request): - id_ = uuid.UUID(request.matchdict['id']) + id_ = uuid.UUID(request.matchdict["id"]) item = request.dbsession.query(Product).filter(Product.id == id_).first() - return product_info(item) + return product_info(item.id, request.dbsession) -@view_config(request_method='GET', route_name='v1_products_list', renderer='json', permission='Authenticated') +@view_config( + request_method="GET", + route_name="v1_products_list", + renderer="json", + permission="Authenticated", +) def show_list(request): - active = request.GET.get('a', None) + active = request.GET.get("a", None) list_ = request.dbsession.query(Product) if active is not None: list_ = list_.filter(Product.is_active == active) - list_ = list_.order_by(Product.product_group_id, Product.sort_order).order_by(Product.name).all() + list_ = ( + list_.order_by(Product.product_group_id, Product.sort_order) + .order_by(Product.name) + .all() + ) products = [] for item in list_: - products.append(product_info(item)) + products.append(product_info(item, request.dbsession)) return products -@view_config(request_method='POST', route_name='v1_products_list', renderer='json', permission='Products', trans=True) +@view_config( + request_method="POST", + route_name="v1_products_list", + renderer="json", + permission="Products", + trans=True, +) def sort_order(request): json = request.json_body - index, last_group = 0, None + indexes = {} for item in json: - product_id = uuid.UUID(item['id']) - product_group_id = uuid.UUID(item['ProductGroupID']) - if last_group != product_group_id: - index = 0 - last_group = product_group_id - request.dbsession.query( - Product - ).filter( - Product.id == product_id - ).update( - {Product.sort_order: index} + product_id = uuid.UUID(item["id"]) + product_group_id = uuid.UUID(item["productGroup"]["id"]) + if product_group_id in indexes: + indexes[product_group_id] += 1 + else: + indexes[product_group_id] = 0 + request.dbsession.query(Product).filter(Product.id == product_id).update( + {Product.sort_order: indexes[product_group_id]} ) return True -def product_info(item): +def product_info(item, dbsession): + if item is None: + return { + "name": "", + "units": "", + "productGroup": {}, + "tax": {}, + "price": 0, + "hasHappyHour": False, + "isNotAvailable": False, + "isActive": True, + "sortOrder": 0, + } + if type(item) is uuid.UUID: + item = dbsession.query(Product).filter(Product.id == item).first() return { - 'id': item.id, - 'name': item.name, - 'units': item.units, - 'productGroup': {'id': item.product_group_id, 'name': item.product_group.name}, - 'tax': {'id': item.vat_id, 'name': item.vat.name, 'rate': item.vat.rate}, - 'price': item.price, - 'hasHappyHour': item.has_happy_hour, - 'isNotAvailable': item.is_not_available, - 'isActive': item.is_active, - 'sortOrder': item.sort_order + "id": item.id, + "name": item.name, + "units": item.units, + "productGroup": {"id": item.product_group_id, "name": item.product_group.name}, + "tax": {"id": item.tax_id, "name": item.tax.name, "rate": item.tax.rate}, + "price": item.price, + "hasHappyHour": item.has_happy_hour, + "isNotAvailable": item.is_not_available, + "isActive": item.is_active, + "sortOrder": item.sort_order, } diff --git a/bookie/src/app/app-routing.module.ts b/bookie/src/app/app-routing.module.ts index cf2f12d..f3291a7 100644 --- a/bookie/src/app/app-routing.module.ts +++ b/bookie/src/app/app-routing.module.ts @@ -11,7 +11,7 @@ const routes: Routes = [ }, { path: 'products', - loadChildren: () => import('./product/product.module').then(mod => mod.ProductModule) + loadChildren: () => import('./product/products.module').then(mod => mod.ProductsModule) }, { path: 'product-groups', diff --git a/bookie/src/app/home/home.component.html b/bookie/src/app/home/home.component.html index ff09da5..ddd5dba 100644 --- a/bookie/src/app/home/home.component.html +++ b/bookie/src/app/home/home.component.html @@ -13,6 +13,9 @@ Product Groups + + Products + Taxes diff --git a/bookie/src/app/product-group/product-group-list/product-group-list-datasource.ts b/bookie/src/app/product-group/product-group-list/product-group-list-datasource.ts index 8e33b2c..01adb0e 100644 --- a/bookie/src/app/product-group/product-group-list/product-group-list-datasource.ts +++ b/bookie/src/app/product-group/product-group-list/product-group-list-datasource.ts @@ -1,15 +1,19 @@ import { DataSource } from '@angular/cdk/collections'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs'; import { ProductGroup } from '../../core/product-group'; +import { tap } from "rxjs/operators"; export class ProductGroupListDataSource extends DataSource { - - constructor(public data: ProductGroup[]) { + private data: ProductGroup[]; + constructor(private readonly dataObs: Observable) { super(); + this.dataObs = dataObs.pipe( + tap(x => this.data = x) + ); } connect(): Observable { - return observableOf(this.data); + return this.dataObs; } disconnect() { diff --git a/bookie/src/app/product-group/product-group-list/product-group-list.component.ts b/bookie/src/app/product-group/product-group-list/product-group-list.component.ts index 746ab1d..b4d7e25 100644 --- a/bookie/src/app/product-group/product-group-list/product-group-list.component.ts +++ b/bookie/src/app/product-group/product-group-list/product-group-list.component.ts @@ -1,11 +1,12 @@ -import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { ProductGroupListDataSource } from './product-group-list-datasource'; import { ProductGroup } from '../../core/product-group'; -import {ActivatedRoute, Router} from '@angular/router'; -import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop"; -import {MatTable} from "@angular/material"; -import {ToasterService} from "../../core/toaster.service"; -import {ProductGroupService} from "../product-group.service"; +import { ActivatedRoute, Router } from '@angular/router'; +import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop"; +import { MatTable } from "@angular/material"; +import { ToasterService } from "../../core/toaster.service"; +import { ProductGroupService } from "../product-group.service"; +import { BehaviorSubject } from "rxjs"; @Component({ selector: 'app-product-group-list', @@ -16,6 +17,7 @@ export class ProductGroupListComponent implements OnInit { @ViewChild('table', { static: true }) table: MatTable; dataSource: ProductGroupListDataSource; list: ProductGroup[]; + data: BehaviorSubject; /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['name', 'discountLimit', 'isModifierCompulsory', 'groupType', 'isActive', 'isFixture']; @@ -24,15 +26,19 @@ export class ProductGroupListComponent implements OnInit { private router: Router, private toaster: ToasterService, private ser: ProductGroupService - ) { - } + ) { + this.data = new BehaviorSubject([]); + this.data.subscribe((data: ProductGroup[]) => { + this.list = data; + }) + } ngOnInit() { this.route.data .subscribe((data: { list: ProductGroup[] }) => { - this.list = data.list; + this.data.next(data.list); }); - this.dataSource = new ProductGroupListDataSource(this.list); + this.dataSource = new ProductGroupListDataSource(this.data); } updateSortOrder() { @@ -47,9 +53,10 @@ export class ProductGroupListComponent implements OnInit { } ); } + dropTable(event: CdkDragDrop) { const prevIndex = this.list.indexOf(event.item.data); moveItemInArray(this.list, prevIndex, event.currentIndex); - this.table.renderRows(); + this.data.next(this.list); } } diff --git a/bookie/src/app/product/product-detail/product-detail.component.html b/bookie/src/app/product/product-detail/product-detail.component.html index 71d1c84..824c5a2 100644 --- a/bookie/src/app/product/product-detail/product-detail.component.html +++ b/bookie/src/app/product/product-detail/product-detail.component.html @@ -26,34 +26,18 @@
- Fraction - + Price + - - Fraction Units - - - - Yield - - -
-
- {{item.isPurchased ? 'Purchase Price' : 'Cost Price' }} - - - - Sale Price - + Quantity +
- Is Purchased? - Is Sold? + Has Happy Hour? + Is Not Available? Is Active?
+ + Tax + + + {{ t.name }} + + +
diff --git a/bookie/src/app/product/product-detail/product-detail.component.ts b/bookie/src/app/product/product-detail/product-detail.component.ts index ae0cc1e..21f3689 100644 --- a/bookie/src/app/product/product-detail/product-detail.component.ts +++ b/bookie/src/app/product/product-detail/product-detail.component.ts @@ -1,12 +1,13 @@ -import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; -import {ToasterService} from '../../core/toaster.service'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ProductService} from '../product.service'; -import {Product} from '../../core/product'; -import {ProductGroup} from '../../core/product-group'; -import {ConfirmDialogComponent} from '../../shared/confirm-dialog/confirm-dialog.component'; +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { ToasterService } from '../../core/toaster.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ProductService } from '../product.service'; +import { Product } from '../../core/product'; +import { ProductGroup } from '../../core/product-group'; +import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { MatDialog } from '@angular/material/dialog'; -import {FormBuilder, FormGroup} from '@angular/forms'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Tax } from "../../core/tax"; @Component({ selector: 'app-product-detail', @@ -17,6 +18,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit { @ViewChild('nameElement', { static: true }) nameElement: ElementRef; form: FormGroup; productGroups: ProductGroup[]; + taxes: Tax[]; item: Product; constructor( @@ -35,22 +37,21 @@ export class ProductDetailComponent implements OnInit, AfterViewInit { code: {value: '', disabled: true}, name: '', units: '', - fraction: '', - fractionUnits: '', - productYield: '', + productGroup: '', + tax: '', price: '', - salePrice: '', - isPurchased: '', - isSold: '', - isActive: '', - productGroup: '' + hasHappyHour: '', + isNotAvailable: '', + quantity: '', + isActive: '' }); } ngOnInit() { this.route.data - .subscribe((data: { item: Product, productGroups: ProductGroup[] }) => { + .subscribe((data: { item: Product, productGroups: ProductGroup[], taxes: Tax[] }) => { this.productGroups = data.productGroups; + this.taxes = data.taxes; this.showItem(data.item); }); } @@ -64,7 +65,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit { productGroup: this.item.tax.id ? this.item.tax.id : '', tax: this.item.productGroup.id ? this.item.productGroup.id : '', price: this.item.price || '', - hadHappyHour: this.item.hasHappyHour, + hasHappyHour: this.item.hasHappyHour, isNotAvailable: this.item.isNotAvailable, quantity: this.item.quantity || '', isActive: this.item.isActive, diff --git a/bookie/src/app/product/product-list-resolver.service.spec.ts b/bookie/src/app/product/product-list-resolver.service.spec.ts index 41f0290..e9e1da9 100644 --- a/bookie/src/app/product/product-list-resolver.service.spec.ts +++ b/bookie/src/app/product/product-list-resolver.service.spec.ts @@ -1,15 +1,15 @@ import {inject, TestBed} from '@angular/core/testing'; -import {ProductListResolverService} from './product-list-resolver.service'; +import {ProductListResolver} from './product-list-resolver.service'; -describe('ProductListResolverService', () => { +describe('ProductListResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ProductListResolverService] + providers: [ProductListResolver] }); }); - it('should be created', inject([ProductListResolverService], (service: ProductListResolverService) => { + it('should be created', inject([ProductListResolver], (service: ProductListResolver) => { expect(service).toBeTruthy(); })); }); diff --git a/bookie/src/app/product/product-list/product-list-datasource.ts b/bookie/src/app/product/product-list/product-list-datasource.ts index 4c479c2..74561d5 100644 --- a/bookie/src/app/product/product-list/product-list-datasource.ts +++ b/bookie/src/app/product/product-list/product-list-datasource.ts @@ -1,34 +1,37 @@ -import {DataSource} from '@angular/cdk/collections'; -import { MatPaginator } from '@angular/material/paginator'; -import {map, tap} from 'rxjs/operators'; -import {merge, Observable, of as observableOf} from 'rxjs'; -import {Product} from '../../core/product'; +import { DataSource } from '@angular/cdk/collections'; +import { map, tap } from 'rxjs/operators'; +import { merge, Observable } from 'rxjs'; +import { Product } from '../../core/product'; export class ProductListDataSource extends DataSource { - private dataObservable: Observable; + public data: Product[]; + public viewData: Product[]; private filterValue: string; - constructor(private paginator: MatPaginator, private filter: Observable, public data: Product[]) { + constructor(private readonly filter: Observable, private readonly dataObs: Observable) { super(); + this.data = []; + this.viewData = []; this.filter = filter.pipe( tap(x => this.filterValue = x) ); + this.dataObs = dataObs.pipe( + tap(x => this.data = x) + ); } connect(): Observable { - this.dataObservable = observableOf(this.data); const dataMutations = [ - this.dataObservable, - this.filter, - this.paginator.page + this.dataObs, + this.filter ]; return merge(...dataMutations).pipe( map((x: any) => { - return this.getPagedData(this.getFilteredData([...this.data])); - }), - tap((x: Product[]) => this.paginator.length = x.length) + this.viewData = this.getFilteredData([...this.data]); + return this.viewData; + }) ); } @@ -36,20 +39,10 @@ export class ProductListDataSource extends DataSource { } private getFilteredData(data: Product[]): Product[] { - const filter = (this.filterValue === undefined) ? '' : this.filterValue; - return filter.split(' ').reduce((p: Product[], c: string) => { - return p.filter(x => { - const productString = ( - x.code + ' ' + x.name + ' ' + x.units + ' ' + x.productGroup + (x.isActive ? 'active' : 'deactive') - ).toLowerCase(); - return productString.indexOf(c) !== -1; + const filter = (this.filterValue === undefined) ? "" : this.filterValue; + return data.filter(x => { + return x.productGroup.id === filter || filter === ""; } ); - }, Object.assign([], data)); - } - - private getPagedData(data: Product[]) { - const startIndex = this.paginator.pageIndex * this.paginator.pageSize; - return data.splice(startIndex, this.paginator.pageSize); } } diff --git a/bookie/src/app/product/product-list/product-list.component.html b/bookie/src/app/product/product-list/product-list.component.html index 1b9589b..b5976ab 100644 --- a/bookie/src/app/product/product-list/product-list.component.html +++ b/bookie/src/app/product/product-list/product-list.component.html @@ -1,7 +1,10 @@ Products - + @@ -15,8 +18,8 @@ fxLayoutGap.lt-md="0px"> Product Type - - -- + + -- All Products -- {{ pg.name }} @@ -24,7 +27,7 @@ - + @@ -41,13 +44,13 @@ Product Group - {{row.productGroup}} + {{row.productGroup.name}} Tax - {{row.tax}} + {{row.tax.rate | percent:'1.2-2'}} {{row.tax.name}} @@ -84,14 +87,7 @@ - + - - - diff --git a/bookie/src/app/product/product-list/product-list.component.ts b/bookie/src/app/product/product-list/product-list.component.ts index 044de22..3194173 100644 --- a/bookie/src/app/product/product-list/product-list.component.ts +++ b/bookie/src/app/product/product-list/product-list.component.ts @@ -1,11 +1,15 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { MatPaginator } from '@angular/material/paginator'; -import { ProductListDataSource } from './product-list-datasource'; -import { Product } from '../../core/product'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { Observable } from 'rxjs'; +import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop"; +import { ProductListDataSource } from './product-list-datasource'; +import { MatTable } from "@angular/material"; +import { Product } from '../../core/product'; import { ToCsvService } from "../../shared/to-csv.service"; +import { ToasterService } from "../../core/toaster.service"; +import { ProductService } from "../product.service"; +import { ProductGroup } from "../../core/product-group"; +import { BehaviorSubject } from "rxjs"; @Component({ selector: 'app-product-list', @@ -13,29 +17,64 @@ import { ToCsvService } from "../../shared/to-csv.service"; styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { - @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild('table', { static: true }) table: MatTable; dataSource: ProductListDataSource; - filter: Observable = new Observable(); + filter: BehaviorSubject; form: FormGroup; list: Product[]; + data: BehaviorSubject; + productGroups: ProductGroup[]; /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns: string[] = ['name', 'price', 'productGroup', 'tax', 'info', 'quantity']; - constructor(private route: ActivatedRoute, private fb: FormBuilder, private toCsv: ToCsvService) { + constructor( + private route: ActivatedRoute, + private fb: FormBuilder, + private router: Router, + private toaster: ToasterService, + private toCsv: ToCsvService, + private ser: ProductService + ) { this.form = this.fb.group({ productGroup: '' }); + this.filter = new BehaviorSubject(""); + this.data = new BehaviorSubject([]); + this.data.subscribe((data: Product[]) => { + this.list = data; + }) } - filterOn(val: string) { - console.log(val); + + filterOn(val: any) { + this.filter.next(val.value); } ngOnInit() { this.route.data - .subscribe((data: { list: Product[] }) => { - this.list = data.list; + .subscribe((data: { list: Product[], productGroups: ProductGroup[] }) => { + this.data.next(data.list); + this.productGroups = data.productGroups; }); - this.dataSource = new ProductListDataSource(this.paginator, this.filter, this.list); + this.dataSource = new ProductListDataSource(this.filter, this.data); + } + + updateSortOrder() { + this.ser.updateSortOrder(this.dataSource.viewData) + .subscribe( + (result) => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/products'); + }, + (error) => { + this.toaster.show('Danger', error.error); + } + ); + } + + dropTable(event: CdkDragDrop) { + const prevIndex = this.list.indexOf(event.item.data); + moveItemInArray(this.list, prevIndex, event.currentIndex); + this.data.next(this.list); } exportCsv() { @@ -48,7 +87,7 @@ export class ProductListComponent implements OnInit { Tax: 'tax' }; - const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {type: 'text/csv;charset=utf-8;'}); + const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.viewData)], {type: 'text/csv;charset=utf-8;'}); const link = document.createElement('a'); link.href = window.URL.createObjectURL(csvData); link.setAttribute('download', 'products.csv'); diff --git a/bookie/src/app/product/product-resolver.service.spec.ts b/bookie/src/app/product/product-resolver.service.spec.ts index d58b20d..6b63a8a 100644 --- a/bookie/src/app/product/product-resolver.service.spec.ts +++ b/bookie/src/app/product/product-resolver.service.spec.ts @@ -1,15 +1,15 @@ import {inject, TestBed} from '@angular/core/testing'; -import {ProductResolverService} from './product-resolver.service'; +import {ProductResolver} from './product-resolver.service'; -describe('ProductResolverService', () => { +describe('ProductResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ProductDetailResolverService] + providers: [ProductResolver] }); }); - it('should be created', inject([ProductDetailResolverService], (service: ProductDetailResolverService) => { + it('should be created', inject([ProductResolver], (service: ProductResolver) => { expect(service).toBeTruthy(); })); }); diff --git a/bookie/src/app/product/product-routing.module.spec.ts b/bookie/src/app/product/product-routing.module.spec.ts deleted file mode 100644 index 3f054cf..0000000 --- a/bookie/src/app/product/product-routing.module.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {ProductRoutingModule} from './product-routing.module'; - -describe('ProductRoutingModule', () => { - let productRoutingModule: ProductRoutingModule; - - beforeEach(() => { - productRoutingModule = new ProductRoutingModule(); - }); - - it('should create an instance', () => { - expect(productRoutingModule).toBeTruthy(); - }); -}); diff --git a/bookie/src/app/product/product-routing.module.ts b/bookie/src/app/product/product-routing.module.ts deleted file mode 100644 index 0b0b470..0000000 --- a/bookie/src/app/product/product-routing.module.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {RouterModule, Routes} from '@angular/router'; - -import {ProductListResolver} from './product-list-resolver.service'; -import {ProductResolver} from './product-resolver.service'; -import {ProductDetailComponent} from './product-detail/product-detail.component'; -import {ProductListComponent} from './product-list/product-list.component'; - -import {AuthGuard} from '../auth/auth-guard.service'; -import {ProductGroupListResolver} from '../product-group/product-group-list-resolver.service'; - -const productRoutes: Routes = [ - { - path: '', - component: ProductListComponent, - canActivate: [AuthGuard], - data: { - permission: 'Products' - }, - resolve: { - list: ProductListResolver - } - }, - { - path: 'new', - component: ProductDetailComponent, - canActivate: [AuthGuard], - data: { - permission: 'Products' - }, - resolve: { - item: ProductResolver, - productGroups: ProductGroupListResolver - } - }, - { - path: ':id', - component: ProductDetailComponent, - canActivate: [AuthGuard], - data: { - permission: 'Products' - }, - resolve: { - item: ProductResolver, - productGroups: ProductGroupListResolver - } - } -]; - -@NgModule({ - imports: [ - CommonModule, - RouterModule.forChild(productRoutes) - - ], - exports: [ - RouterModule - ], - providers: [ - ProductListResolver, - ProductResolver - ] -}) -export class ProductRoutingModule { -} diff --git a/bookie/src/app/product/product.module.spec.ts b/bookie/src/app/product/product.module.spec.ts deleted file mode 100644 index 0ec58f1..0000000 --- a/bookie/src/app/product/product.module.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {ProductModule} from './product.module'; - -describe('ProductModule', () => { - let productModule: ProductModule; - - beforeEach(() => { - productModule = new ProductModule(); - }); - - it('should create an instance', () => { - expect(productModule).toBeTruthy(); - }); -}); diff --git a/bookie/src/app/product/product.service.ts b/bookie/src/app/product/product.service.ts index 4ecbc5d..2fecb02 100644 --- a/bookie/src/app/product/product.service.ts +++ b/bookie/src/app/product/product.service.ts @@ -1,9 +1,9 @@ -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs/internal/Observable'; -import {catchError} from 'rxjs/operators'; -import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; -import {Product} from '../core/product'; -import {ErrorLoggerService} from '../core/error-logger.service'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { catchError } from 'rxjs/operators'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Product } from '../core/product'; +import { ErrorLoggerService } from '../core/error-logger.service'; const httpOptions = { headers: new HttpHeaders({'Content-Type': 'application/json'}) @@ -48,6 +48,13 @@ export class ProductService { ); } + updateSortOrder(list: Product[]): Observable { + return >this.http.post(url, list, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'updateSortOrder')) + ); + } + saveOrUpdate(product: Product): Observable { if (!product.id) { return this.save(product); diff --git a/bookie/src/app/product/products-routing.module.spec.ts b/bookie/src/app/product/products-routing.module.spec.ts new file mode 100644 index 0000000..712ce45 --- /dev/null +++ b/bookie/src/app/product/products-routing.module.spec.ts @@ -0,0 +1,13 @@ +import {ProductsRoutingModule} from './products-routing.module'; + +describe('ProductsRoutingModule', () => { + let productsRoutingModule: ProductsRoutingModule; + + beforeEach(() => { + productsRoutingModule = new ProductsRoutingModule(); + }); + + it('should create an instance', () => { + expect(productsRoutingModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product/products-routing.module.ts b/bookie/src/app/product/products-routing.module.ts new file mode 100644 index 0000000..8ace696 --- /dev/null +++ b/bookie/src/app/product/products-routing.module.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { ProductListResolver } from './product-list-resolver.service'; +import { ProductResolver } from './product-resolver.service'; +import { ProductDetailComponent } from './product-detail/product-detail.component'; +import { ProductListComponent } from './product-list/product-list.component'; + +import { AuthGuard } from '../auth/auth-guard.service'; +import { ProductGroupListResolver } from '../product-group/product-group-list-resolver.service'; +import { TaxListResolver } from "../taxes/tax-list-resolver.service"; + +const productRoutes: Routes = [ + { + path: '', + component: ProductListComponent, + canActivate: [AuthGuard], + data: { + permission: 'Products' + }, + resolve: { + list: ProductListResolver, + productGroups: ProductGroupListResolver + } + }, + { + path: 'new', + component: ProductDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Products' + }, + resolve: { + item: ProductResolver, + productGroups: ProductGroupListResolver, + taxes: TaxListResolver + } + }, + { + path: ':id', + component: ProductDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Products' + }, + resolve: { + item: ProductResolver, + productGroups: ProductGroupListResolver, + taxes: TaxListResolver + } + } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(productRoutes) + + ], + exports: [ + RouterModule + ], + providers: [ + ProductListResolver, + ProductResolver + ] +}) +export class ProductsRoutingModule { +} diff --git a/bookie/src/app/product/products.module.spec.ts b/bookie/src/app/product/products.module.spec.ts new file mode 100644 index 0000000..4532102 --- /dev/null +++ b/bookie/src/app/product/products.module.spec.ts @@ -0,0 +1,13 @@ +import {ProductsModule} from './products.module'; + +describe('ProductsModule', () => { + let productsModule: ProductsModule; + + beforeEach(() => { + productsModule = new ProductsModule(); + }); + + it('should create an instance', () => { + expect(productsModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product/product.module.ts b/bookie/src/app/product/products.module.ts similarity index 78% rename from bookie/src/app/product/product.module.ts rename to bookie/src/app/product/products.module.ts index 961a628..ed2b228 100644 --- a/bookie/src/app/product/product.module.ts +++ b/bookie/src/app/product/products.module.ts @@ -3,7 +3,7 @@ import {CommonModule} from '@angular/common'; import {ProductListComponent} from './product-list/product-list.component'; import {ProductDetailComponent} from './product-detail/product-detail.component'; -import {ProductRoutingModule} from './product-routing.module'; +import {ProductsRoutingModule} from './products-routing.module'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -15,14 +15,16 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSelectModule } from '@angular/material/select'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; -import {CdkTableModule} from '@angular/cdk/table'; -import {ReactiveFormsModule} from '@angular/forms'; -import {FlexLayoutModule} from '@angular/flex-layout'; +import { CdkTableModule } from '@angular/cdk/table'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { DragDropModule } from "@angular/cdk/drag-drop"; @NgModule({ imports: [ CommonModule, CdkTableModule, + DragDropModule, FlexLayoutModule, MatTableModule, MatPaginatorModule, @@ -36,12 +38,12 @@ import {FlexLayoutModule} from '@angular/flex-layout'; MatSelectModule, MatCheckboxModule, ReactiveFormsModule, - ProductRoutingModule + ProductsRoutingModule ], declarations: [ ProductListComponent, ProductDetailComponent ] }) -export class ProductModule { +export class ProductsModule { }