diff --git a/barker/models/master.py b/barker/models/master.py index d5b6484..39e6871 100644 --- a/barker/models/master.py +++ b/barker/models/master.py @@ -53,7 +53,7 @@ class FoodTable(Base): def __name__(self): return self.name - def __init__(self, name=None, location=None, is_active=None, sort_order=None, id=None): + def __init__(self, name=None, location=None, is_active=None, sort_order=0, id=None): self.name = name self.location = location self.is_active = is_active @@ -67,10 +67,12 @@ class Tax(Base): 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, id=None): + def __init__(self, name=None, rate=None, is_fixture=False, id=None): self.name = name self.rate = rate + self.is_fixture = is_fixture self.id = id @@ -84,6 +86,7 @@ class ProductGroup(Base): 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) def __init__(self, name, discount_limit, is_modifier_compulsory, group_type, is_active, sort_order, id=None): @@ -104,8 +107,7 @@ class Product(Base): 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) - vat_id = Column('vat_id', GUID(), ForeignKey('taxes.id'), nullable=False) - service_tax_id = Column('service_tax_id', GUID(), ForeignKey('taxes.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) @@ -115,16 +117,14 @@ class Product(Base): sort_order = Column('sort_order', Numeric, nullable=False) product_group = relationship('ProductGroup', backref='products') - vat = relationship('Tax', foreign_keys=vat_id) - service_tax = relationship('Tax', foreign_keys=service_tax_id) + tax = relationship('Tax', foreign_keys=tax_id) - def __init__(self, name=None, units=None, product_group_id=None, vat_id=None, service_tax_id=None, price=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=None, id=None): self.name = name self.units = units self.product_group_id = product_group_id - self.vat_id = vat_id - self.service_tax_id = service_tax_id + self.tax_id = tax_id self.price = price self.has_happy_hour = has_happy_hour self.is_not_available = is_not_available diff --git a/barker/models/validation_exception.py b/barker/models/validation_exception.py new file mode 100644 index 0000000..2e80b40 --- /dev/null +++ b/barker/models/validation_exception.py @@ -0,0 +1,11 @@ +class ValidationError(Exception): + def __init__(self, message, errors=None): + self.message = message + # Call the base class constructor with the parameters it needs + Exception.__init__(self, message) + + # Now for your custom code... + self.errors = errors + + def __str__(self): + return self.message diff --git a/barker/routes.py b/barker/routes.py index e66ab34..a1944ff 100644 --- a/barker/routes.py +++ b/barker/routes.py @@ -40,6 +40,20 @@ def includeme(config): config.add_route('print_location_list', '/PrintLocations.json') config.add_route('print_location_id', '/PrintLocation/{id}.json') + config.add_route("products_new", "/products/new") + config.add_route("products_id", "/products/{id}") + config.add_route("products_list", "/products") + config.add_route("v1_products_new", "/v1/products/new") + config.add_route("v1_products_id", "/v1/products/{id}") + config.add_route("v1_products_list", "/v1/products") + + config.add_route("product_groups_new", "/product-groups/new") + config.add_route("product_groups_id", "/product-groups/{id}") + config.add_route("product_groups_list", "/product-groups") + config.add_route("v1_product_groups_new", "/v1/product-groups/new") + config.add_route("v1_product_groups_id", "/v1/product-groups/{id}") + config.add_route("v1_product_groups_list", "/v1/product-groups") + config.add_route('product', '/Product.json') config.add_route('product_list', '/Products.json') config.add_route('product_id', '/Product/{id}.json') @@ -64,9 +78,12 @@ def includeme(config): config.add_route('sa_st', '/SaleAnalysis/ServiceTax.json') config.add_route('sa_vat', '/SaleAnalysis/Vat.json') - config.add_route('tax', '/Tax.json') - config.add_route('tax_list', '/Taxes.json') - config.add_route('tax_id', '/Tax/{id}.json') + config.add_route("taxes_new", "/taxes/new") + config.add_route("taxes_id", "/taxes/{id}") + config.add_route("taxes_list", "/taxes") + config.add_route("v1_taxes_new", "/v1/taxes/new") + config.add_route("v1_taxes_id", "/v1/taxes/{id}") + config.add_route("v1_taxes_list", "/v1/taxes") config.add_route('v1_users_new', '/v1/users/new') config.add_route('v1_users_list', '/v1/users/list') diff --git a/barker/scripts/fixtures.py b/barker/scripts/fixtures.py index eaa1ff3..a9c9b2a 100644 --- a/barker/scripts/fixtures.py +++ b/barker/scripts/fixtures.py @@ -14,6 +14,7 @@ from barker.models import ( DbSetting, Permission, User, + FoodTable, get_engine, get_session_factory, get_tm_session, @@ -108,6 +109,9 @@ def main(argv=sys.argv): admin.roles.append(roles[0]) dbsession.add(admin) + for name in range(1,20): + dbsession.add(FoodTable(str(name), "", True)) + options = [ SettleOption('Unsettled', False, 1, True, 1), SettleOption('Cash', True, 2, False, 2), diff --git a/barker/views/product.py b/barker/views/product.py index de30d3e..1fbc66a 100644 --- a/barker/views/product.py +++ b/barker/views/product.py @@ -1,45 +1,103 @@ import uuid +from decimal import Decimal, InvalidOperation import transaction from pyramid.response import Response from pyramid.view import view_config from barker.models import Product +from barker.models.validation_exception import ValidationError -@view_config(request_method='PUT', route_name='product', renderer='json', permission='Products', trans=True) +@view_config(request_method='PUT', route_name='v1_products_new', renderer='json', permission='Products', trans=True) def save(request): json = request.json_body - item = Product(json['Name'], json['Units'], json['ProductGroupID'], json['VatID'], json['ServiceTaxID'], - json['Price'], json['HasHappyHour'], json['IsNotAvailable'], json['Quantity'], json['IsActive'], - json['SortOrder']) + + name = json.get("name", "").strip() + if name == "": + raise ValidationError("Name cannot be blank") + + units = json.get("units", "").strip() + + product_group = json.get("productGroup", None) + if product_group is None: + raise ValidationError("please choose a product group") + product_group_id = uuid.UUID(product_group["id"]) + + tax = json.get("tax", None) + if product_group is None: + raise ValidationError("please choose a product group") + tax_id = uuid.UUID(tax["id"]) + + try: + price = Decimal(json.get("price", 0)) + if price < 0: + raise ValidationError("Price must be a decimal >= 0") + except (ValueError, InvalidOperation): + raise ValidationError("Price must be a decimal >= 0") + + has_happy_hour = json.get("hasHappyHour", True) + is_not_available = json.get("isNotAvailable", True) + + try: + quantity = Decimal(json.get("quantity", 0)) + if quantity < 0: + raise ValidationError("Quantity must be a decimal >= 0") + except (ValueError, InvalidOperation): + raise ValidationError("Quantity must be a decimal >= 0") + + is_active = json.get("isActive", True) + + item = Product( + name, + units, + product_group_id, + tax_id, + price, + has_happy_hour, + is_not_available, + quantity, + is_active + ) request.dbsession.add(item) transaction.commit() item = request.dbsession.query(Product).filter(Product.id == item.id).first() return product_info(item) -@view_config(request_method='POST', route_name='product_id', renderer='json', permission='Products', trans=True) +@view_config(request_method='POST', route_name='v1_products_id', renderer='json', permission='Products', trans=True) def update(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() + ) json = request.json_body - item.name = json['Name'] - item.units = json['Units'] - item.product_group_id = json['ProductGroupID'] - item.vat_id = json['VatID'] - item.service_tax_id = json['ServiceTaxID'] - item.price = json['Price'] - item.has_happy_hour = json['HasHappyHour'] - item.is_not_available = json['IsNotAvailable'] - item.quantity = json['Quantity'] - item.is_active = json['IsActive'] - item.sort_order = json['SortOrder'] + 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"]) + 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'] + 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'] transaction.commit() item = request.dbsession.query(Product).filter(Product.id == item.id).first() return product_info(item) -@view_config(request_method='DELETE', route_name='product_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() if item is None: @@ -52,32 +110,32 @@ def delete(request): return response -@view_config(request_method='GET', route_name='product_id', renderer='json', permission='Authenticated') +@view_config(request_method='GET', route_name='v1_products_id', renderer='json', permission='Authenticated') def show_id(request): - id = uuid.UUID(request.matchdict['id']) - item = request.dbsession.query(Product).filter(Product.id == id).first() + id_ = uuid.UUID(request.matchdict['id']) + item = request.dbsession.query(Product).filter(Product.id == id_).first() return product_info(item) -@view_config(request_method='GET', route_name='product_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) - list = request.dbsession.query(Product) + 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_.filter(Product.is_active == active) + list_ = list_.order_by(Product.product_group_id, Product.sort_order).order_by(Product.name).all() products = [] - for item in list: + for item in list_: products.append(product_info(item)) return products -@view_config(request_method='POST', route_name='product_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 for item in json: - product_id = uuid.UUID(item['ProductID']) + product_id = uuid.UUID(item['id']) product_group_id = uuid.UUID(item['ProductGroupID']) if last_group != product_group_id: index = 0 @@ -94,16 +152,14 @@ def sort_order(request): def product_info(item): return { - 'ProductID': item.id, - 'Name': item.name, - 'Units': item.units, - 'ProductGroup': {'ProductGroupID': item.product_group_id, 'Name': item.product_group.name}, - 'Vat': {'TaxID': item.vat_id, 'Name': item.vat.name, 'Rate': item.vat.rate}, - 'ServiceTax': {'TaxID': item.service_tax_id, 'Name': item.service_tax.name, 'Rate': item.service_tax.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.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 } diff --git a/barker/views/tax.py b/barker/views/tax.py index 2533904..5312d03 100644 --- a/barker/views/tax.py +++ b/barker/views/tax.py @@ -1,63 +1,109 @@ import uuid +from decimal import Decimal, InvalidOperation import transaction from pyramid.view import view_config from barker.models import Tax +from barker.models.validation_exception import ValidationError -@view_config(request_method='PUT', route_name='tax', renderer='json', permission='Taxes', trans=True) +@view_config(request_method='POST', route_name='v1_taxes_new', renderer='json', permission='Taxes', trans=True) def save(request): json = request.json_body - item = Tax(json['Name'], json['Rate']) + name = json.get("name", "").strip() + if name == "": + raise ValidationError("Name cannot be blank") + try: + rate = Decimal(json["rate"]) / 100 + if rate < 0: + raise ValidationError("Tax Rate must be a decimal >= 0") + except (ValueError, InvalidOperation): + raise ValidationError("Tax Rate must be a decimal >= 0") + item = Tax(name, rate) request.dbsession.add(item) transaction.commit() - item = request.dbsession.query(Tax).filter(Tax.id == item.id).first() - return tax_info(item) + return tax_info(item.id, request.dbsession) -@view_config(request_method='POST', route_name='tax_id', renderer='json', permission='Taxes', trans=True) +@view_config(request_method='PUT', route_name='v1_taxes_id', renderer='json', permission='Taxes', trans=True) def update(request): json = request.json_body - item = request.dbsession.query(Tax).filter(Tax.id == uuid.UUID(request.matchdict['id'])).first() - item.name = json['Name'] - item.rate = json['Rate'] + item = ( + request.dbsession.query(Tax) + .filter(Tax.id == uuid.UUID(request.matchdict["id"])) + .first() + ) + if item.is_fixture: + raise ValidationError( + "{0} is a fixture and cannot be edited or deleted.".format(item.full_name) + ) + item.name = json['name'].strip() + try: + item.rate = Decimal(json["rate"]) / 100 + if item.rate <= 0: + raise ValidationError("Tax Rate must be a decimal >= 0") + except (ValueError, InvalidOperation): + raise ValidationError("Tax Rate must be a decimal >= 0") + item.rate = json['rate'] transaction.commit() item = request.dbsession.query(Tax).filter(Tax.id == item.id).first() - return tax_info(item) + return tax_info(item.id, request.dbsession) -@view_config(request_method='DELETE', route_name='tax_id', renderer='json', permission='Taxes', trans=True) +@view_config(request_method='DELETE', route_name='v1_taxes_id', renderer='json', permission='Taxes', trans=True) def delete(request): - item = request.dbsession.query(Tax).filter(Tax.id == uuid.UUID(request.matchdict['id'])).first() + item = ( + request.dbsession.query(Tax) + .filter(Tax.id == uuid.UUID(request.matchdict["id"])) + .first() + ) + if item.is_fixture: + raise ValidationError( + "{0} is a fixture and cannot be edited or deleted.".format(item.full_name) + ) request.dbsession.delete(item) transaction.commit() - return {} + return tax_info(None, request.dbsession) -@view_config(request_method='GET', route_name='tax_id', renderer='json', permission='Authenticated') +@view_config(request_method='GET', route_name='v1_taxes_new', renderer='json', permission='Authenticated') +def show_blank(request): + return tax_info(None, request.dbsession) + + +@view_config(request_method='GET', route_name='v1_taxes_id', renderer='json', permission='Authenticated') def show_id(request): - id = uuid.UUID(request.matchdict['id']) - item = request.dbsession.query(Tax).filter(Tax.id == id).first() - return tax_info(item) + return tax_info(uuid.UUID(request.matchdict["id"]), request.dbsession) -@view_config(request_method='GET', route_name='tax_list', renderer='json', permission='Authenticated') +@view_config(request_method='GET', route_name='v1_taxes_list', renderer='json', permission='Authenticated') def show_list(request): - list = request.dbsession.query(Tax).order_by(Tax.name).all() + list_ = request.dbsession.query(Tax).order_by(Tax.name).all() taxes = [] - for item in list: + for item in list_: taxes.append({ - 'TaxID': item.id, - 'Name': item.name, - 'Rate': item.rate, + 'id': item.id, + 'name': item.name, + 'rate': item.rate, + 'isFixture': item.is_fixture }) return taxes -def tax_info(item): - return { - 'TaxID': item.id, - 'Name': item.name, - 'Rate': item.rate - } +def tax_info(id_, dbsession): + if id_ is None: + tax = { + "name": "", + "rate": 0, + "isFixture": False + } + else: + item = dbsession.query(Tax).filter(Tax.id == id_).first() + tax = { + 'id': item.id, + 'name': item.name, + 'rate': item.rate * 100, + 'isFixture': item.is_fixture + } + return tax diff --git a/bookie/src/app/app-routing.module.ts b/bookie/src/app/app-routing.module.ts index 25e1961..518fd3c 100644 --- a/bookie/src/app/app-routing.module.ts +++ b/bookie/src/app/app-routing.module.ts @@ -9,6 +9,14 @@ const routes: Routes = [ path: 'guest-book', loadChildren: () => import('./guest-book/guest-book.module').then(mod => mod.GuestBookModule) }, + { + path: 'products', + loadChildren: () => import('./product/product.module').then(mod => mod.ProductModule) + }, + { + path: 'product-groups', + loadChildren: () => import('./product-group/product-group.module').then(mod => mod.ProductGroupModule) + }, { path: 'running-tables', loadChildren: () => import('./running-tables/running-tables.module').then(mod => mod.RunningTablesModule) @@ -17,6 +25,10 @@ const routes: Routes = [ path: 'tables', loadChildren: () => import('./tables/tables.module').then(mod => mod.TableModule) }, + { + path: 'taxes', + loadChildren: () => import('./taxes/taxes.module').then(mod => mod.TaxesModule) + }, {path: 'login', component: LoginComponent}, {path: 'logout', component: LogoutComponent}, {path: '', component: HomeComponent}, diff --git a/bookie/src/app/core/product-group.ts b/bookie/src/app/core/product-group.ts new file mode 100644 index 0000000..51ab0e2 --- /dev/null +++ b/bookie/src/app/core/product-group.ts @@ -0,0 +1,10 @@ +export class ProductGroup { + id: string; + name: string; + discountLimit: number; + isModifierCompulsory: boolean; + groupModifier: string; + isActive: boolean; + isFixture: boolean; + sortOrder: number; +} diff --git a/bookie/src/app/core/product.ts b/bookie/src/app/core/product.ts new file mode 100644 index 0000000..9db6b69 --- /dev/null +++ b/bookie/src/app/core/product.ts @@ -0,0 +1,17 @@ +import {ProductGroup} from './product-group'; +import {Tax} from "./tax"; + +export class Product { + id: string; + code: number; + name: string; + units: string; + productGroup: ProductGroup; + tax: Tax; + price: number; + hasHappyHour: boolean; + isNotAvailable: boolean; + quantity: number; + isActive: boolean; + sortOrder: number; +} diff --git a/bookie/src/app/core/tax.ts b/bookie/src/app/core/tax.ts new file mode 100644 index 0000000..a286cfb --- /dev/null +++ b/bookie/src/app/core/tax.ts @@ -0,0 +1,6 @@ +export class Tax { + id: string; + name: string; + rate: number; + isFixture: boolean; +} diff --git a/bookie/src/app/home/home.component.html b/bookie/src/app/home/home.component.html index ebf0418..3a3dda9 100644 --- a/bookie/src/app/home/home.component.html +++ b/bookie/src/app/home/home.component.html @@ -10,6 +10,9 @@ Tables + + Taxes + account_box Logout {{name}} diff --git a/bookie/src/app/product-group/product-group-detail/product-group-detail.component.css b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.css new file mode 100644 index 0000000..82c7afd --- /dev/null +++ b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.css @@ -0,0 +1,3 @@ +.example-card { + max-width: 400px; +} diff --git a/bookie/src/app/product-group/product-group-detail/product-group-detail.component.html b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.html new file mode 100644 index 0000000..a8c0167 --- /dev/null +++ b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.html @@ -0,0 +1,21 @@ +
+ + + Product Group + + +
+
+ + Name + + +
+
+
+ + + +
+
diff --git a/bookie/src/app/product-group/product-group-detail/product-group-detail.component.spec.ts b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.spec.ts new file mode 100644 index 0000000..5060015 --- /dev/null +++ b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.spec.ts @@ -0,0 +1,25 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProductGroupDetailComponent} from './product-group-detail.component'; + +describe('ProductGroupDetailComponent', () => { + let component: ProductGroupDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ProductGroupDetailComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductGroupDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product-group/product-group-detail/product-group-detail.component.ts b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.ts new file mode 100644 index 0000000..a1b3c28 --- /dev/null +++ b/bookie/src/app/product-group/product-group-detail/product-group-detail.component.ts @@ -0,0 +1,73 @@ +import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; + +import {ProductGroupService} from '../product-group.service'; +import {ProductGroup} from '../../core/product-group'; +import {ActivatedRoute, Router} from '@angular/router'; +import {ToasterService} from '../../core/toaster.service'; +import {FormBuilder, FormGroup} from '@angular/forms'; + +@Component({ + selector: 'app-product-group-detail', + templateUrl: './product-group-detail.component.html', + styleUrls: ['./product-group-detail.component.css'] +}) +export class ProductGroupDetailComponent implements OnInit, AfterViewInit { + @ViewChild('nameElement', { static: true }) nameElement: ElementRef; + form: FormGroup; + item: ProductGroup; + + constructor( + private route: ActivatedRoute, + private router: Router, + private fb: FormBuilder, + private toaster: ToasterService, + private ser: ProductGroupService + ) { + this.createForm(); + } + + createForm() { + this.form = this.fb.group({ + name: '' + }); + } + + ngOnInit() { + this.route.data + .subscribe((data: { item: ProductGroup }) => { + this.showItem(data.item); + }); + } + + showItem(item: ProductGroup) { + this.item = item; + this.form.setValue({ + name: this.item.name, + }); + } + + ngAfterViewInit() { + setTimeout(() => { + this.nameElement.nativeElement.focus(); + }, 0); + } + + save() { + this.ser.saveOrUpdate(this.getItem()) + .subscribe( + (result) => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/product-groups'); + }, + (error) => { + this.toaster.show('Danger', error.error); + } + ); + } + + getItem(): ProductGroup { + const formModel = this.form.value; + this.item.name = formModel.name; + return this.item; + } +} diff --git a/bookie/src/app/product-group/product-group-list-resolver.service.spec.ts b/bookie/src/app/product-group/product-group-list-resolver.service.spec.ts new file mode 100644 index 0000000..864d865 --- /dev/null +++ b/bookie/src/app/product-group/product-group-list-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductGroupListResolverService} from './product-group-list-resolver.service'; + +describe('ProductGroupListResolverService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductGroupListResolverService] + }); + }); + + it('should be created', inject([ProductGroupListResolverService], (service: ProductGroupListResolverService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product-group/product-group-list-resolver.service.ts b/bookie/src/app/product-group/product-group-list-resolver.service.ts new file mode 100644 index 0000000..e27be64 --- /dev/null +++ b/bookie/src/app/product-group/product-group-list-resolver.service.ts @@ -0,0 +1,18 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router'; +import {ProductGroup} from '../core/product-group'; +import {Observable} from 'rxjs/internal/Observable'; +import {ProductGroupService} from './product-group.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductGroupListResolver implements Resolve { + + constructor(private ser: ProductGroupService, private router: Router) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.ser.list(); + } +} 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 new file mode 100644 index 0000000..6f81453 --- /dev/null +++ b/bookie/src/app/product-group/product-group-list/product-group-list-datasource.ts @@ -0,0 +1,59 @@ +import {DataSource} from '@angular/cdk/collections'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import {map} from 'rxjs/operators'; +import {merge, Observable, of as observableOf} from 'rxjs'; +import {ProductGroup} from '../../core/product-group'; + +export class ProductGroupListDataSource extends DataSource { + + constructor(private paginator: MatPaginator, private sort: MatSort, public data: ProductGroup[]) { + super(); + } + + connect(): Observable { + const dataMutations = [ + observableOf(this.data), + this.paginator.page, + this.sort.sortChange + ]; + + // Set the paginators length + this.paginator.length = this.data.length; + + return merge(...dataMutations).pipe(map(() => { + return this.getPagedData(this.getSortedData([...this.data])); + })); + } + + disconnect() { + } + + private getPagedData(data: ProductGroup[]) { + const startIndex = this.paginator.pageIndex * this.paginator.pageSize; + return data.splice(startIndex, this.paginator.pageSize); + } + + private getSortedData(data: ProductGroup[]) { + if (!this.sort.active || this.sort.direction === '') { + return data; + } + + return data.sort((a, b) => { + const isAsc = this.sort.direction === 'asc'; + switch (this.sort.active) { + case 'name': + return compare(a.name, b.name, isAsc); + case 'id': + return compare(+a.id, +b.id, isAsc); + default: + return 0; + } + }); + } +} + +/** Simple sort comparator for example ID/Name columns (for client-side sorting). */ +function compare(a, b, isAsc) { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); +} diff --git a/bookie/src/app/product-group/product-group-list/product-group-list.component.css b/bookie/src/app/product-group/product-group-list/product-group-list.component.css new file mode 100644 index 0000000..e69de29 diff --git a/bookie/src/app/product-group/product-group-list/product-group-list.component.html b/bookie/src/app/product-group/product-group-list/product-group-list.component.html new file mode 100644 index 0000000..f62d2f8 --- /dev/null +++ b/bookie/src/app/product-group/product-group-list/product-group-list.component.html @@ -0,0 +1,35 @@ + + + Product Groups +
+ add_box + Add + + + + + + + + Name + {{row.name}} + + + + + Is Fixture? + {{row.isFixture}} + + + + + + + + + + diff --git a/bookie/src/app/product-group/product-group-list/product-group-list.component.spec.ts b/bookie/src/app/product-group/product-group-list/product-group-list.component.spec.ts new file mode 100644 index 0000000..939ebec --- /dev/null +++ b/bookie/src/app/product-group/product-group-list/product-group-list.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; + +import {ProductGroupListComponent} from './product-group-list.component'; + +describe('ProductGroupListComponent', () => { + let component: ProductGroupListComponent; + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [ProductGroupListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProductGroupListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000..eee7c7a --- /dev/null +++ b/bookie/src/app/product-group/product-group-list/product-group-list.component.ts @@ -0,0 +1,31 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import {ProductGroupListDataSource} from './product-group-list-datasource'; +import {ProductGroup} from '../../core/product-group'; +import {ActivatedRoute} from '@angular/router'; + +@Component({ + selector: 'app-product-group-list', + templateUrl: './product-group-list.component.html', + styleUrls: ['./product-group-list.component.css'] +}) +export class ProductGroupListComponent implements OnInit { + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + dataSource: ProductGroupListDataSource; + list: ProductGroup[]; + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ + displayedColumns = ['name', 'isFixture']; + + constructor(private route: ActivatedRoute) { + } + + ngOnInit() { + this.route.data + .subscribe((data: { list: ProductGroup[] }) => { + this.list = data.list; + }); + this.dataSource = new ProductGroupListDataSource(this.paginator, this.sort, this.list); + } +} diff --git a/bookie/src/app/product-group/product-group-resolver.service.spec.ts b/bookie/src/app/product-group/product-group-resolver.service.spec.ts new file mode 100644 index 0000000..fea6971 --- /dev/null +++ b/bookie/src/app/product-group/product-group-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductGroupResolverService} from './product-group-resolver.service'; + +describe('ProductGroupResolverService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductGroupDetailResolverService] + }); + }); + + it('should be created', inject([ProductGroupDetailResolverService], (service: ProductGroupDetailResolverService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product-group/product-group-resolver.service.ts b/bookie/src/app/product-group/product-group-resolver.service.ts new file mode 100644 index 0000000..85e2ff8 --- /dev/null +++ b/bookie/src/app/product-group/product-group-resolver.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router'; +import {ProductGroupService} from './product-group.service'; +import {ProductGroup} from '../core/product-group'; +import {Observable} from 'rxjs/internal/Observable'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductGroupResolver implements Resolve { + + constructor(private ser: ProductGroupService, private router: Router) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const id = route.paramMap.get('id'); + return this.ser.get(id); + } +} diff --git a/bookie/src/app/product-group/product-group-routing.module.spec.ts b/bookie/src/app/product-group/product-group-routing.module.spec.ts new file mode 100644 index 0000000..88d6e8f --- /dev/null +++ b/bookie/src/app/product-group/product-group-routing.module.spec.ts @@ -0,0 +1,13 @@ +import {ProductGroupRoutingModule} from './product-group-routing.module'; + +describe('ProductGroupRoutingModule', () => { + let productGroupRoutingModule: ProductGroupRoutingModule; + + beforeEach(() => { + productGroupRoutingModule = new ProductGroupRoutingModule(); + }); + + it('should create an instance', () => { + expect(productGroupRoutingModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product-group/product-group-routing.module.ts b/bookie/src/app/product-group/product-group-routing.module.ts new file mode 100644 index 0000000..c6d0cdd --- /dev/null +++ b/bookie/src/app/product-group/product-group-routing.module.ts @@ -0,0 +1,61 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {RouterModule, Routes} from '@angular/router'; +import {ProductGroupListResolver} from './product-group-list-resolver.service'; +import {ProductGroupResolver} from './product-group-resolver.service'; +import {ProductGroupListComponent} from './product-group-list/product-group-list.component'; +import {ProductGroupDetailComponent} from './product-group-detail/product-group-detail.component'; +import {AuthGuard} from '../auth/auth-guard.service'; + +const productGroupRoutes: Routes = [ + { + path: '', + component: ProductGroupListComponent, + canActivate: [AuthGuard], + data: { + permission: 'Cost Centres' + }, + resolve: { + list: ProductGroupListResolver + } + }, + { + path: 'new', + component: ProductGroupDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Cost Centres' + }, + resolve: { + item: ProductGroupResolver + } + }, + { + path: ':id', + component: ProductGroupDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Cost Centres' + }, + resolve: { + item: ProductGroupResolver + } + } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(productGroupRoutes) + + ], + exports: [ + RouterModule + ], + providers: [ + ProductGroupListResolver, + ProductGroupResolver + ] +}) +export class ProductGroupRoutingModule { +} diff --git a/bookie/src/app/product-group/product-group.module.spec.ts b/bookie/src/app/product-group/product-group.module.spec.ts new file mode 100644 index 0000000..40eb41d --- /dev/null +++ b/bookie/src/app/product-group/product-group.module.spec.ts @@ -0,0 +1,13 @@ +import {ProductGroupModule} from './product-group.module'; + +describe('ProductGroupModule', () => { + let productGroupModule: ProductGroupModule; + + beforeEach(() => { + productGroupModule = new ProductGroupModule(); + }); + + it('should create an instance', () => { + expect(productGroupModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product-group/product-group.module.ts b/bookie/src/app/product-group/product-group.module.ts new file mode 100644 index 0000000..f8dbdcb --- /dev/null +++ b/bookie/src/app/product-group/product-group.module.ts @@ -0,0 +1,41 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {ProductGroupListComponent} from './product-group-list/product-group-list.component'; +import {ProductGroupDetailComponent} from './product-group-detail/product-group-detail.component'; +import {ProductGroupRoutingModule} from './product-group-routing.module'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import {CdkTableModule} from '@angular/cdk/table'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {ReactiveFormsModule} from '@angular/forms'; + +@NgModule({ + imports: [ + CommonModule, + CdkTableModule, + FlexLayoutModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatCardModule, + MatProgressSpinnerModule, + MatInputModule, + MatButtonModule, + MatIconModule, + ReactiveFormsModule, + ProductGroupRoutingModule + ], + declarations: [ + ProductGroupListComponent, + ProductGroupDetailComponent + ] +}) +export class ProductGroupModule { +} diff --git a/bookie/src/app/product-group/product-group.service.spec.ts b/bookie/src/app/product-group/product-group.service.spec.ts new file mode 100644 index 0000000..9f9dd7d --- /dev/null +++ b/bookie/src/app/product-group/product-group.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductGroupService} from './product-group.service'; + +describe('ProductGroupService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductGroupService] + }); + }); + + it('should be created', inject([ProductGroupService], (service: ProductGroupService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product-group/product-group.service.ts b/bookie/src/app/product-group/product-group.service.ts new file mode 100644 index 0000000..afb00e4 --- /dev/null +++ b/bookie/src/app/product-group/product-group.service.ts @@ -0,0 +1,65 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; +import {ErrorLoggerService} from '../core/error-logger.service'; +import {catchError} from 'rxjs/operators'; +import {Observable} from 'rxjs/internal/Observable'; +import {ProductGroup} from '../core/product-group'; + +const httpOptions = { + headers: new HttpHeaders({'Content-Type': 'application/json'}) +}; +const url = '/api/product-groups'; +const serviceName = 'ProductGroupService'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductGroupService { + constructor(private http: HttpClient, private log: ErrorLoggerService) { + } + + get(id: string): Observable { + const getUrl: string = (id === null) ? `${url}/new` : `${url}/${id}`; + return >this.http.get(getUrl) + .pipe( + catchError(this.log.handleError(serviceName, `get id=${id}`)) + ); + } + + list(): Observable { + const options = {params: new HttpParams().set('l', '')}; + return >this.http.get(url, options) + .pipe( + catchError(this.log.handleError(serviceName, 'list')) + ); + } + + save(productGroup: ProductGroup): Observable { + return >this.http.post(`${url}/new`, productGroup, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'save')) + ); + } + + update(productGroup: ProductGroup): Observable { + return >this.http.put(`${url}/${productGroup.id}`, productGroup, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'update')) + ); + } + + saveOrUpdate(productGroup: ProductGroup): Observable { + if (!productGroup.id) { + return this.save(productGroup); + } else { + return this.update(productGroup); + } + } + + delete(id: string): Observable { + return >this.http.delete(`${url}/${id}`, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'delete')) + ); + } +} diff --git a/bookie/src/app/product/product-detail/product-detail.component.css b/bookie/src/app/product/product-detail/product-detail.component.css new file mode 100644 index 0000000..e69de29 diff --git a/bookie/src/app/product/product-detail/product-detail.component.html b/bookie/src/app/product/product-detail/product-detail.component.html new file mode 100644 index 0000000..71d1c84 --- /dev/null +++ b/bookie/src/app/product/product-detail/product-detail.component.html @@ -0,0 +1,77 @@ +
+ + + Product + + +
+
+ + Code + + +
+
+ + Name + + + + Units + + +
+
+ + Fraction + + + + Fraction Units + + + + Yield + + +
+
+ + {{item.isPurchased ? 'Purchase Price' : 'Cost Price' }} + + + + Sale Price + + +
+
+ Is Purchased? + Is Sold? + Is Active? +
+
+ + Product Type + + + {{ pg.name }} + + + +
+
+
+ + + + +
+
diff --git a/bookie/src/app/product/product-detail/product-detail.component.spec.ts b/bookie/src/app/product/product-detail/product-detail.component.spec.ts new file mode 100644 index 0000000..83e97a9 --- /dev/null +++ b/bookie/src/app/product/product-detail/product-detail.component.spec.ts @@ -0,0 +1,25 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProductDetailComponent} from './product-detail.component'; + +describe('ProductDetailComponent', () => { + let component: ProductDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ProductDetailComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product/product-detail/product-detail.component.ts b/bookie/src/app/product/product-detail/product-detail.component.ts new file mode 100644 index 0000000..ae0cc1e --- /dev/null +++ b/bookie/src/app/product/product-detail/product-detail.component.ts @@ -0,0 +1,132 @@ +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'; + +@Component({ + selector: 'app-product-detail', + templateUrl: './product-detail.component.html', + styleUrls: ['./product-detail.component.css'] +}) +export class ProductDetailComponent implements OnInit, AfterViewInit { + @ViewChild('nameElement', { static: true }) nameElement: ElementRef; + form: FormGroup; + productGroups: ProductGroup[]; + item: Product; + + constructor( + private route: ActivatedRoute, + private router: Router, + private dialog: MatDialog, + private fb: FormBuilder, + private toaster: ToasterService, + private ser: ProductService + ) { + this.createForm(); + } + + createForm() { + this.form = this.fb.group({ + code: {value: '', disabled: true}, + name: '', + units: '', + fraction: '', + fractionUnits: '', + productYield: '', + price: '', + salePrice: '', + isPurchased: '', + isSold: '', + isActive: '', + productGroup: '' + }); + } + + ngOnInit() { + this.route.data + .subscribe((data: { item: Product, productGroups: ProductGroup[] }) => { + this.productGroups = data.productGroups; + this.showItem(data.item); + }); + } + + showItem(item: Product) { + this.item = item; + this.form.setValue({ + code: this.item.code || '(Auto)', + name: this.item.name || '', + units: this.item.units || '', + 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, + isNotAvailable: this.item.isNotAvailable, + quantity: this.item.quantity || '', + isActive: this.item.isActive, + }); + } + + ngAfterViewInit() { + setTimeout(() => { + this.nameElement.nativeElement.focus(); + }, 0); + } + + save() { + this.ser.saveOrUpdate(this.getItem()) + .subscribe( + (result) => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/products'); + }, + (error) => { + this.toaster.show('Danger', error.error); + } + ); + } + + delete() { + this.ser.delete(this.item.id) + .subscribe( + (result) => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/products'); + }, + (error) => { + this.toaster.show('Danger', error.error); + } + ); + } + + confirmDelete(): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '250px', + data: {title: 'Delete Product?', content: 'Are you sure? This cannot be undone.'} + }); + + dialogRef.afterClosed().subscribe((result: boolean) => { + if (result) { + this.delete(); + } + }); + } + + getItem(): Product { + const formModel = this.form.value; + this.item.name = formModel.name; + this.item.units = formModel.units; + this.item.productGroup.id = formModel.productGroup; + this.item.tax.id = formModel.tax; + this.item.price = +formModel.price; + this.item.hasHappyHour = formModel.hasHappyHour; + this.item.isNotAvailable = formModel.isNotAvailable; + this.item.quantity = +formModel.quantity; + this.item.isActive = formModel.isActive; + return this.item; + } +} diff --git a/bookie/src/app/product/product-list-resolver.service.spec.ts b/bookie/src/app/product/product-list-resolver.service.spec.ts new file mode 100644 index 0000000..41f0290 --- /dev/null +++ b/bookie/src/app/product/product-list-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductListResolverService} from './product-list-resolver.service'; + +describe('ProductListResolverService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductListResolverService] + }); + }); + + it('should be created', inject([ProductListResolverService], (service: ProductListResolverService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product/product-list-resolver.service.ts b/bookie/src/app/product/product-list-resolver.service.ts new file mode 100644 index 0000000..0e1a710 --- /dev/null +++ b/bookie/src/app/product/product-list-resolver.service.ts @@ -0,0 +1,18 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Product} from '../core/product'; +import {Observable} from 'rxjs/internal/Observable'; +import {ProductService} from './product.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductListResolver implements Resolve { + + constructor(private ser: ProductService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.ser.list(); + } +} diff --git a/bookie/src/app/product/product-list/product-list-datasource.ts b/bookie/src/app/product/product-list/product-list-datasource.ts new file mode 100644 index 0000000..4c479c2 --- /dev/null +++ b/bookie/src/app/product/product-list/product-list-datasource.ts @@ -0,0 +1,55 @@ +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'; + + +export class ProductListDataSource extends DataSource { + private dataObservable: Observable; + private filterValue: string; + + constructor(private paginator: MatPaginator, private filter: Observable, public data: Product[]) { + super(); + this.filter = filter.pipe( + tap(x => this.filterValue = x) + ); + } + + connect(): Observable { + this.dataObservable = observableOf(this.data); + const dataMutations = [ + this.dataObservable, + this.filter, + this.paginator.page + ]; + + return merge(...dataMutations).pipe( + map((x: any) => { + return this.getPagedData(this.getFilteredData([...this.data])); + }), + tap((x: Product[]) => this.paginator.length = x.length) + ); + } + + disconnect() { + } + + 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; + } + ); + }, 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.css b/bookie/src/app/product/product-list/product-list.component.css new file mode 100644 index 0000000..e69de29 diff --git a/bookie/src/app/product/product-list/product-list.component.html b/bookie/src/app/product/product-list/product-list.component.html new file mode 100644 index 0000000..1b9589b --- /dev/null +++ b/bookie/src/app/product/product-list/product-list.component.html @@ -0,0 +1,97 @@ + + + Products + + + add_box + Add + + + +
+
+ + Product Type + + -- + + {{ pg.name }} + + + +
+
+ + + + + Name + {{row.name}} ({{row.units}}) + + + + + Price + {{row.price | currency:'INR'}} + + + + + Product Group + {{row.productGroup}} + + + + + Tax + {{row.tax}} + + + + + Details + +
+
+ + {{ row.hasHappyHour ? "sentiment_satisfied_alt" : "sentiment_dissatisfied" }} + + {{ row.hasHappyHour ? "Happy Hour" : "Regular" }} +
+
+ + {{ row.isNotAvailable ? "pause" : "play_arrow" }} + + {{ row.isNotAvailable ? "Not Available" : "Available" }} +
+
+ + {{ row.isActive ? "visibility" : "visibility_off" }} + + {{ row.isActive ? "Active" : "Deactivated" }} +
+
+
+
+ + + + Quantity + {{row.quantity | currency:'INR'}} + + + + +
+ + + +
+
diff --git a/bookie/src/app/product/product-list/product-list.component.spec.ts b/bookie/src/app/product/product-list/product-list.component.spec.ts new file mode 100644 index 0000000..06504af --- /dev/null +++ b/bookie/src/app/product/product-list/product-list.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; + +import {ProductListComponent} from './product-list.component'; + +describe('ProductListComponent', () => { + let component: ProductListComponent; + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [ProductListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProductListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product/product-list/product-list.component.ts b/bookie/src/app/product/product-list/product-list.component.ts new file mode 100644 index 0000000..044de22 --- /dev/null +++ b/bookie/src/app/product/product-list/product-list.component.ts @@ -0,0 +1,59 @@ +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 { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { ToCsvService } from "../../shared/to-csv.service"; + +@Component({ + selector: 'app-product-list', + templateUrl: './product-list.component.html', + styleUrls: ['./product-list.component.css'] +}) +export class ProductListComponent implements OnInit { + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + dataSource: ProductListDataSource; + filter: Observable = new Observable(); + form: FormGroup; + list: Product[]; + /** 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) { + this.form = this.fb.group({ + productGroup: '' + }); + } + filterOn(val: string) { + console.log(val); + } + + ngOnInit() { + this.route.data + .subscribe((data: { list: Product[] }) => { + this.list = data.list; + }); + this.dataSource = new ProductListDataSource(this.paginator, this.filter, this.list); + } + + exportCsv() { + const headers = { + Code: 'code', + Name: 'name', + Units: 'units', + Price: 'price', + ProductGroup: 'productGroup', + Tax: 'tax' + }; + + const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {type: 'text/csv;charset=utf-8;'}); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(csvData); + link.setAttribute('download', 'products.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } +} diff --git a/bookie/src/app/product/product-resolver.service.spec.ts b/bookie/src/app/product/product-resolver.service.spec.ts new file mode 100644 index 0000000..d58b20d --- /dev/null +++ b/bookie/src/app/product/product-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductResolverService} from './product-resolver.service'; + +describe('ProductResolverService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductDetailResolverService] + }); + }); + + it('should be created', inject([ProductDetailResolverService], (service: ProductDetailResolverService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product/product-resolver.service.ts b/bookie/src/app/product/product-resolver.service.ts new file mode 100644 index 0000000..349e005 --- /dev/null +++ b/bookie/src/app/product/product-resolver.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router'; +import {ProductService} from './product.service'; +import {Product} from '../core/product'; +import {Observable} from 'rxjs/internal/Observable'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductResolver implements Resolve { + + constructor(private ser: ProductService, private router: Router) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const id = route.paramMap.get('id'); + return this.ser.get(id); + } +} diff --git a/bookie/src/app/product/product-routing.module.spec.ts b/bookie/src/app/product/product-routing.module.spec.ts new file mode 100644 index 0000000..3f054cf --- /dev/null +++ b/bookie/src/app/product/product-routing.module.spec.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..0b0b470 --- /dev/null +++ b/bookie/src/app/product/product-routing.module.ts @@ -0,0 +1,66 @@ +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 new file mode 100644 index 0000000..0ec58f1 --- /dev/null +++ b/bookie/src/app/product/product.module.spec.ts @@ -0,0 +1,13 @@ +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.module.ts b/bookie/src/app/product/product.module.ts new file mode 100644 index 0000000..961a628 --- /dev/null +++ b/bookie/src/app/product/product.module.ts @@ -0,0 +1,47 @@ +import {NgModule} from '@angular/core'; +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 { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatOptionModule } from '@angular/material/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatPaginatorModule } from '@angular/material/paginator'; +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'; + +@NgModule({ + imports: [ + CommonModule, + CdkTableModule, + FlexLayoutModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatCardModule, + MatProgressSpinnerModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatOptionModule, + MatSelectModule, + MatCheckboxModule, + ReactiveFormsModule, + ProductRoutingModule + ], + declarations: [ + ProductListComponent, + ProductDetailComponent + ] +}) +export class ProductModule { +} diff --git a/bookie/src/app/product/product.service.spec.ts b/bookie/src/app/product/product.service.spec.ts new file mode 100644 index 0000000..9a94723 --- /dev/null +++ b/bookie/src/app/product/product.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductService} from './product.service'; + +describe('ProductService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductService] + }); + }); + + it('should be created', inject([ProductService], (service: ProductService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product/product.service.ts b/bookie/src/app/product/product.service.ts new file mode 100644 index 0000000..4ecbc5d --- /dev/null +++ b/bookie/src/app/product/product.service.ts @@ -0,0 +1,81 @@ +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'}) +}; + +const url = '/v1/products'; +const serviceName = 'ProductService'; + +@Injectable({providedIn: 'root'}) +export class ProductService { + + constructor(private http: HttpClient, private log: ErrorLoggerService) { + } + + get(id: string): Observable { + const getUrl: string = (id === null) ? `${url}/new` : `${url}/${id}`; + return >this.http.get(getUrl) + .pipe( + catchError(this.log.handleError(serviceName, `get id=${id}`)) + ); + } + + list(): Observable { + const options = {params: new HttpParams().set('l', '')}; + return >this.http.get(url, options) + .pipe( + catchError(this.log.handleError(serviceName, 'getList')) + ); + } + + save(product: Product): Observable { + return >this.http.post(`${url}/new`, product, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'save')) + ); + } + + update(product: Product): Observable { + return >this.http.put(`${url}/${product.id}`, product, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'update')) + ); + } + + saveOrUpdate(product: Product): Observable { + if (!product.id) { + return this.save(product); + } else { + return this.update(product); + } + } + + delete(id: string): Observable { + return >this.http.delete(`${url}/${id}`, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'delete')) + ); + } + + autocomplete(term: string): Observable { + const options = {params: new HttpParams().set('t', term)}; + return >this.http.get(url, options) + .pipe( + catchError(this.log.handleError(serviceName, 'autocomplete')) + ); + } + + balance(id: string, date: string): Observable { + const options = {params: new HttpParams().set('b', 'true').set('d', date)}; + return >this.http.get(`${url}/${id}`, options) + .pipe( + catchError(this.log.handleError(serviceName, 'balance')) + ); + } +} diff --git a/bookie/src/app/products/products-routing.module.ts b/bookie/src/app/products/products-routing.module.ts deleted file mode 100644 index 4b34bbe..0000000 --- a/bookie/src/app/products/products-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class ProductsRoutingModule { } diff --git a/bookie/src/app/products/products.module.ts b/bookie/src/app/products/products.module.ts deleted file mode 100644 index 75623ee..0000000 --- a/bookie/src/app/products/products.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { ProductsRoutingModule } from './products-routing.module'; - -@NgModule({ - declarations: [], - imports: [ - CommonModule, - ProductsRoutingModule - ] -}) -export class ProductsModule { } diff --git a/bookie/src/app/taxes/tax-detail/tax-detail.component.css b/bookie/src/app/taxes/tax-detail/tax-detail.component.css new file mode 100644 index 0000000..12052db --- /dev/null +++ b/bookie/src/app/taxes/tax-detail/tax-detail.component.css @@ -0,0 +1,3 @@ +.right-align { + text-align: right; +} diff --git a/bookie/src/app/taxes/tax-detail/tax-detail.component.html b/bookie/src/app/taxes/tax-detail/tax-detail.component.html new file mode 100644 index 0000000..80db79a --- /dev/null +++ b/bookie/src/app/taxes/tax-detail/tax-detail.component.html @@ -0,0 +1,30 @@ +
+ + + Tax + + +
+
+ + Name + + +
+
+ + Rate + + % + +
+
+
+ + + + +
+
diff --git a/bookie/src/app/taxes/tax-detail/tax-detail.component.spec.ts b/bookie/src/app/taxes/tax-detail/tax-detail.component.spec.ts new file mode 100644 index 0000000..6336e92 --- /dev/null +++ b/bookie/src/app/taxes/tax-detail/tax-detail.component.spec.ts @@ -0,0 +1,25 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {TaxDetailComponent} from './tax-detail.component'; + +describe('TaxDetailComponent', () => { + let component: TaxDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TaxDetailComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TaxDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/taxes/tax-detail/tax-detail.component.ts b/bookie/src/app/taxes/tax-detail/tax-detail.component.ts new file mode 100644 index 0000000..0a3d6f0 --- /dev/null +++ b/bookie/src/app/taxes/tax-detail/tax-detail.component.ts @@ -0,0 +1,76 @@ +import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; + +import {TaxService} from '../tax.service'; +import {Tax} from '../../core/tax'; +import {ActivatedRoute, Router} from '@angular/router'; +import {ToasterService} from '../../core/toaster.service'; +import {FormBuilder, FormGroup} from '@angular/forms'; + +@Component({ + selector: 'app-tax-detail', + templateUrl: './tax-detail.component.html', + styleUrls: ['./tax-detail.component.css'] +}) +export class TaxDetailComponent implements OnInit, AfterViewInit { + @ViewChild('nameElement', { static: true }) nameElement: ElementRef; + form: FormGroup; + item: Tax; + + constructor( + private route: ActivatedRoute, + private router: Router, + private fb: FormBuilder, + private toaster: ToasterService, + private ser: TaxService + ) { + this.createForm(); + } + + createForm() { + this.form = this.fb.group({ + name: '', + rate: '' + }); + } + + ngOnInit() { + this.route.data + .subscribe((data: { item: Tax }) => { + this.showItem(data.item); + }); + } + + showItem(item: Tax) { + this.item = item; + this.form.setValue({ + name: this.item.name || '', + rate: this.item.rate || '' + }); + } + + ngAfterViewInit() { + setTimeout(() => { + this.nameElement.nativeElement.focus(); + }, 0); + } + + save() { + this.ser.saveOrUpdate(this.getItem()) + .subscribe( + (result) => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/taxes'); + }, + (error) => { + this.toaster.show('Danger', error.error); + } + ); + } + + getItem(): Tax { + const formModel = this.form.value; + this.item.name = formModel.name; + this.item.rate = formModel.rate; + return this.item; + } +} diff --git a/bookie/src/app/taxes/tax-list-resolver.service.spec.ts b/bookie/src/app/taxes/tax-list-resolver.service.spec.ts new file mode 100644 index 0000000..398c27a --- /dev/null +++ b/bookie/src/app/taxes/tax-list-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {TaxListResolver} from './tax-list-resolver.service'; + +describe('TaxListResolverService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [TaxListResolver] + }); + }); + + it('should be created', inject([TaxListResolver], (service: TaxListResolver) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/taxes/tax-list-resolver.service.ts b/bookie/src/app/taxes/tax-list-resolver.service.ts new file mode 100644 index 0000000..b8c1102 --- /dev/null +++ b/bookie/src/app/taxes/tax-list-resolver.service.ts @@ -0,0 +1,18 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router'; +import {Tax} from '../core/tax'; +import {Observable} from 'rxjs/internal/Observable'; +import {TaxService} from './tax.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TaxListResolver implements Resolve { + + constructor(private ser: TaxService, private router: Router) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.ser.list(); + } +} diff --git a/bookie/src/app/taxes/tax-list/tax-list-datasource.ts b/bookie/src/app/taxes/tax-list/tax-list-datasource.ts new file mode 100644 index 0000000..c2d1c5f --- /dev/null +++ b/bookie/src/app/taxes/tax-list/tax-list-datasource.ts @@ -0,0 +1,59 @@ +import {DataSource} from '@angular/cdk/collections'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import {map} from 'rxjs/operators'; +import {merge, Observable, of as observableOf} from 'rxjs'; +import {Tax} from '../../core/tax'; + +export class TaxListDataSource extends DataSource { + + constructor(private paginator: MatPaginator, private sort: MatSort, public data: Tax[]) { + super(); + } + + connect(): Observable { + const dataMutations = [ + observableOf(this.data), + this.paginator.page, + this.sort.sortChange + ]; + + // Set the paginators length + this.paginator.length = this.data.length; + + return merge(...dataMutations).pipe(map(() => { + return this.getPagedData(this.getSortedData([...this.data])); + })); + } + + disconnect() { + } + + private getPagedData(data: Tax[]) { + const startIndex = this.paginator.pageIndex * this.paginator.pageSize; + return data.splice(startIndex, this.paginator.pageSize); + } + + private getSortedData(data: Tax[]) { + if (!this.sort.active || this.sort.direction === '') { + return data; + } + + return data.sort((a, b) => { + const isAsc = this.sort.direction === 'asc'; + switch (this.sort.active) { + case 'name': + return compare(a.name, b.name, isAsc); + case 'id': + return compare(+a.id, +b.id, isAsc); + default: + return 0; + } + }); + } +} + +/** Simple sort comparator for example ID/Name columns (for client-side sorting). */ +function compare(a, b, isAsc) { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); +} diff --git a/bookie/src/app/taxes/tax-list/tax-list.component.css b/bookie/src/app/taxes/tax-list/tax-list.component.css new file mode 100644 index 0000000..e69de29 diff --git a/bookie/src/app/taxes/tax-list/tax-list.component.html b/bookie/src/app/taxes/tax-list/tax-list.component.html new file mode 100644 index 0000000..fab5338 --- /dev/null +++ b/bookie/src/app/taxes/tax-list/tax-list.component.html @@ -0,0 +1,41 @@ + + + Taxes + + add_box + Add + + + + + + + + Name + {{row.name}} + + + + + Rate + {{row.rate | percent:'1.2-2'}} + + + + + Is Fixture? + {{row.isFixture}} + + + + + + + + + + diff --git a/bookie/src/app/taxes/tax-list/tax-list.component.spec.ts b/bookie/src/app/taxes/tax-list/tax-list.component.spec.ts new file mode 100644 index 0000000..6ddf10d --- /dev/null +++ b/bookie/src/app/taxes/tax-list/tax-list.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; + +import {TaxListComponent} from './tax-list.component'; + +describe('TaxListComponent', () => { + let component: TaxListComponent; + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [TaxListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TaxListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/taxes/tax-list/tax-list.component.ts b/bookie/src/app/taxes/tax-list/tax-list.component.ts new file mode 100644 index 0000000..597d073 --- /dev/null +++ b/bookie/src/app/taxes/tax-list/tax-list.component.ts @@ -0,0 +1,31 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import {TaxListDataSource} from './tax-list-datasource'; +import {Tax} from '../../core/tax'; +import {ActivatedRoute} from '@angular/router'; + +@Component({ + selector: 'app-tax-list', + templateUrl: './tax-list.component.html', + styleUrls: ['./tax-list.component.css'] +}) +export class TaxListComponent implements OnInit { + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + dataSource: TaxListDataSource; + list: Tax[]; + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ + displayedColumns = ['name', 'rate', 'isFixture']; + + constructor(private route: ActivatedRoute) { + } + + ngOnInit() { + this.route.data + .subscribe((data: { list: Tax[] }) => { + this.list = data.list; + }); + this.dataSource = new TaxListDataSource(this.paginator, this.sort, this.list); + } +} diff --git a/bookie/src/app/taxes/tax-resolver.service.spec.ts b/bookie/src/app/taxes/tax-resolver.service.spec.ts new file mode 100644 index 0000000..e0023ec --- /dev/null +++ b/bookie/src/app/taxes/tax-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {TaxResolver} from './tax-resolver.service'; + +describe('TaxResolver', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [TaxResolver] + }); + }); + + it('should be created', inject([TaxResolver], (service: TaxResolver) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/taxes/tax-resolver.service.ts b/bookie/src/app/taxes/tax-resolver.service.ts new file mode 100644 index 0000000..475fbd6 --- /dev/null +++ b/bookie/src/app/taxes/tax-resolver.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot} from '@angular/router'; +import {TaxService} from './tax.service'; +import {Tax} from '../core/tax'; +import {Observable} from 'rxjs/internal/Observable'; + +@Injectable({ + providedIn: 'root' +}) +export class TaxResolver implements Resolve { + + constructor(private ser: TaxService, private router: Router) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const id = route.paramMap.get('id'); + return this.ser.get(id); + } +} diff --git a/bookie/src/app/taxes/tax.service.spec.ts b/bookie/src/app/taxes/tax.service.spec.ts new file mode 100644 index 0000000..43b2b61 --- /dev/null +++ b/bookie/src/app/taxes/tax.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {TaxService} from './tax.service'; + +describe('TaxService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [TaxService] + }); + }); + + it('should be created', inject([TaxService], (service: TaxService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/taxes/tax.service.ts b/bookie/src/app/taxes/tax.service.ts new file mode 100644 index 0000000..e57294c --- /dev/null +++ b/bookie/src/app/taxes/tax.service.ts @@ -0,0 +1,65 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; +import {ErrorLoggerService} from '../core/error-logger.service'; +import {catchError} from 'rxjs/operators'; +import {Observable} from 'rxjs/internal/Observable'; +import {Tax} from '../core/tax'; + +const httpOptions = { + headers: new HttpHeaders({'Content-Type': 'application/json'}) +}; +const url = '/v1/taxes'; +const serviceName = 'TaxService'; + +@Injectable({ + providedIn: 'root' +}) +export class TaxService { + constructor(private http: HttpClient, private log: ErrorLoggerService) { + } + + get(id: string): Observable { + const getUrl: string = (id === null) ? `${url}/new` : `${url}/${id}`; + return >this.http.get(getUrl) + .pipe( + catchError(this.log.handleError(serviceName, `get id=${id}`)) + ); + } + + list(): Observable { + const options = {params: new HttpParams().set('l', '')}; + return >this.http.get(url, options) + .pipe( + catchError(this.log.handleError(serviceName, 'list')) + ); + } + + save(tax: Tax): Observable { + return >this.http.post(`${url}/new`, tax, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'save')) + ); + } + + update(tax: Tax): Observable { + return >this.http.put(`${url}/${tax.id}`, tax, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'update')) + ); + } + + saveOrUpdate(tax: Tax): Observable { + if (!tax.id) { + return this.save(tax); + } else { + return this.update(tax); + } + } + + delete(id: string): Observable { + return >this.http.delete(`${url}/${id}`, httpOptions) + .pipe( + catchError(this.log.handleError(serviceName, 'delete')) + ); + } +} diff --git a/bookie/src/app/taxes/taxes-routing.module.spec.ts b/bookie/src/app/taxes/taxes-routing.module.spec.ts new file mode 100644 index 0000000..6d21f59 --- /dev/null +++ b/bookie/src/app/taxes/taxes-routing.module.spec.ts @@ -0,0 +1,13 @@ +import {TaxesRoutingModule} from './taxes-routing.module'; + +describe('TaxesRoutingModule', () => { + let taxesRoutingModule: TaxesRoutingModule; + + beforeEach(() => { + taxesRoutingModule = new TaxesRoutingModule(); + }); + + it('should create an instance', () => { + expect(taxesRoutingModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/taxes/taxes-routing.module.ts b/bookie/src/app/taxes/taxes-routing.module.ts new file mode 100644 index 0000000..e9f7d28 --- /dev/null +++ b/bookie/src/app/taxes/taxes-routing.module.ts @@ -0,0 +1,61 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {RouterModule, Routes} from '@angular/router'; +import {TaxListResolver} from './tax-list-resolver.service'; +import {TaxResolver} from './tax-resolver.service'; +import {TaxListComponent} from './tax-list/tax-list.component'; +import {TaxDetailComponent} from './tax-detail/tax-detail.component'; +import {AuthGuard} from '../auth/auth-guard.service'; + +const taxesRoutes: Routes = [ + { + path: '', + component: TaxListComponent, + canActivate: [AuthGuard], + data: { + permission: 'Taxes' + }, + resolve: { + list: TaxListResolver + } + }, + { + path: 'new', + component: TaxDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Taxes' + }, + resolve: { + item: TaxResolver + } + }, + { + path: ':id', + component: TaxDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Taxes' + }, + resolve: { + item: TaxResolver + } + } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(taxesRoutes) + + ], + exports: [ + RouterModule + ], + providers: [ + TaxListResolver, + TaxResolver + ] +}) +export class TaxesRoutingModule { +} diff --git a/bookie/src/app/taxes/taxes.module.spec.ts b/bookie/src/app/taxes/taxes.module.spec.ts new file mode 100644 index 0000000..a92c057 --- /dev/null +++ b/bookie/src/app/taxes/taxes.module.spec.ts @@ -0,0 +1,13 @@ +import {TaxesModule} from './taxes.module'; + +describe('TaxesModule', () => { + let taxesModule: TaxesModule; + + beforeEach(() => { + taxesModule = new TaxesModule(); + }); + + it('should create an instance', () => { + expect(taxesModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/taxes/taxes.module.ts b/bookie/src/app/taxes/taxes.module.ts new file mode 100644 index 0000000..00fe34c --- /dev/null +++ b/bookie/src/app/taxes/taxes.module.ts @@ -0,0 +1,41 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {TaxListComponent} from './tax-list/tax-list.component'; +import {TaxDetailComponent} from './tax-detail/tax-detail.component'; +import {TaxesRoutingModule} from './taxes-routing.module'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +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'; + +@NgModule({ + imports: [ + CommonModule, + CdkTableModule, + FlexLayoutModule, + MatButtonModule, + MatCardModule, + MatIconModule, + MatInputModule, + MatPaginatorModule, + MatProgressSpinnerModule, + MatSortModule, + MatTableModule, + ReactiveFormsModule, + TaxesRoutingModule + ], + declarations: [ + TaxListComponent, + TaxDetailComponent + ] +}) +export class TaxesModule { +}