Feature: Added product bundles. Have to wire it up to inventory and reports.
This commit is contained in:
1 parent
b79af6e1c6
commit
6e8843d3c4
48 files changed
+2139
-130
No files matched your search
@@ -0,0 +1,16 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { BundleItem } from '../bundle';
|
||||
|
||||
export class BundleDetailDatasource extends DataSource<BundleItem> {
|
||||
constructor(private data: Observable<BundleItem[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BundleItem[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
||||
<h1 mat-dialog-title>Edit Bundle Item</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Item</mat-label>
|
||||
<input matInput type="text" formControlName="product" [matAutocomplete]="autoP" />
|
||||
<mat-autocomplete #autoP="matAutocomplete" [displayWith]="displayFn" (optionSelected)="productSelected($event)">
|
||||
@for (p of products$ | async; track p) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="flex flex-row flex-wrap gap-3">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Quantity</mat-label>
|
||||
<input matInput type="number" formControlName="quantity" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" formControlName="salePrice" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions>
|
||||
<button mat-raised-button color="warn" [mat-dialog-close]="false">Cancel</button>
|
||||
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleDetailDialogComponent } from './bundle-detail-dialog.component';
|
||||
|
||||
describe('BundleDetailDialogComponent', () => {
|
||||
let component: BundleDetailDialogComponent;
|
||||
let fixture: ComponentFixture<BundleDetailDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BundleDetailDialogComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BundleDetailDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { debounceTime, distinctUntilChanged, Observable, of as observableOf, switchMap } from 'rxjs';
|
||||
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { ProductService } from '../../product/product.service';
|
||||
import { BundleItem } from '../bundle';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-detail-dialog',
|
||||
templateUrl: './bundle-detail-dialog.component.html',
|
||||
styleUrls: ['./bundle-detail-dialog.component.css'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class BundleDetailDialogComponent implements OnInit {
|
||||
private readonly dialogRef = inject<MatDialogRef<BundleDetailDialogComponent>>(MatDialogRef);
|
||||
private readonly productSer = inject(ProductService);
|
||||
|
||||
data = inject<{ item: BundleItem }>(MAT_DIALOG_DATA);
|
||||
|
||||
selectedProduct: ProductQuery | null = null;
|
||||
products$: Observable<ProductQuery[]>;
|
||||
|
||||
form: FormGroup<{
|
||||
product: FormControl<ProductQuery | string>;
|
||||
quantity: FormControl<number>;
|
||||
salePrice: FormControl<number>;
|
||||
}>;
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
product: new FormControl<ProductQuery | string>('', { nonNullable: true }),
|
||||
quantity: new FormControl<number>(1, { nonNullable: true }),
|
||||
salePrice: new FormControl<number>(0, { nonNullable: true }),
|
||||
});
|
||||
|
||||
this.products$ = this.form.controls.product.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => {
|
||||
if (typeof x !== 'string') return observableOf([]);
|
||||
return x.trim() === '' ? observableOf([]) : this.productSer.query(x);
|
||||
}),
|
||||
);
|
||||
|
||||
// typing after selecting invalidates prior selection
|
||||
this.form.controls.product.valueChanges.subscribe((x) => {
|
||||
if (typeof x === 'string') this.selectedProduct = null;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// preload display text using existing name
|
||||
this.form.setValue({
|
||||
product: this.data.item.name ?? '',
|
||||
quantity: Number(this.data.item.quantity ?? 1),
|
||||
salePrice: Number(this.data.item.salePrice ?? 0),
|
||||
});
|
||||
|
||||
this.selectedProduct = null;
|
||||
}
|
||||
|
||||
displayFn(product?: ProductQuery | string): string {
|
||||
return !product ? '' : typeof product === 'string' ? product : product.name;
|
||||
}
|
||||
|
||||
productSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const p = event.option.value as ProductQuery;
|
||||
this.selectedProduct = p;
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
const v = this.form.value;
|
||||
|
||||
const quantity = Number(v.quantity ?? 0);
|
||||
if (Number.isNaN(quantity) || quantity <= 0) return;
|
||||
|
||||
const salePrice = Number(v.salePrice ?? 0);
|
||||
if (Number.isNaN(salePrice) || salePrice < 0) return;
|
||||
|
||||
// If a new product was selected, update itemId + name
|
||||
if (this.selectedProduct?.id) {
|
||||
this.data.item.itemId = this.selectedProduct.id;
|
||||
this.data.item.name = this.selectedProduct.name ?? '';
|
||||
} else {
|
||||
// If no selection, don't allow changing SKU silently
|
||||
// but still allow quantity/price edit
|
||||
}
|
||||
|
||||
this.data.item.quantity = quantity;
|
||||
this.data.item.salePrice = salePrice;
|
||||
|
||||
this.dialogRef.close(this.data.item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.nutrition-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
justify-items: stretch;
|
||||
}
|
||||
.nutrition-grid > * {
|
||||
width: 100%;
|
||||
box-sizing: border-box; /* helps with padding */
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<h2>Bundle</h2>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="basis-3-4">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #name formControlName="name" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="basis-1-4">
|
||||
<mat-label>Units</mat-label>
|
||||
<input matInput formControlName="units" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row-container">
|
||||
<mat-form-field class="basis-1-4">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" formControlName="salePrice" readonly="" />
|
||||
<mat-hint>Computed from bundle items</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="basis-2-4">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select formControlName="menuCategory">
|
||||
@for (mc of menuCategories; track mc) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox formControlName="hasHappyHour" class="basis-1-4">Has Happy Hour?</mat-checkbox>
|
||||
<mat-checkbox formControlName="isNotAvailable" class="basis-1-4">Not Available?</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<h2>Items</h2>
|
||||
|
||||
<div formGroupName="addRow" class="nutrition-grid">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Product</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
formControlName="itemId"
|
||||
[matAutocomplete]="autoItem"
|
||||
[value]="displayFn(form.controls.addRow.controls.itemId.value)"
|
||||
/>
|
||||
<mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn" (optionSelected)="bundleSelected($event)">
|
||||
@for (p of itemProducts | async; track p) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Quantity</mat-label>
|
||||
<input matInput type="number" formControlName="quantity" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" formControlName="salePrice" />
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container wrapped">
|
||||
<mat-table [dataSource]="dataSource" aria-label="Bundle Items" class="flex-auto">
|
||||
<!-- Name Column (name is "name (units)") -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Item</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
{{ row.name }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Quantity Column -->
|
||||
<ng-container matColumnDef="quantity">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Qty</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">
|
||||
{{ row.quantity }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Sale Price Column -->
|
||||
<ng-container matColumnDef="salePrice">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Sale Price</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">
|
||||
{{ row.salePrice | currency: 'INR' }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Column -->
|
||||
<ng-container matColumnDef="action">
|
||||
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="center">
|
||||
<button mat-icon-button tabindex="-1" (click)="editRow(row)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { BundleDetailComponent } from './bundle-detail.component';
|
||||
|
||||
describe('BundleDetailComponent', () => {
|
||||
let component: BundleDetailComponent;
|
||||
let fixture: ComponentFixture<BundleDetailComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [BundleDetailComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BundleDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,325 @@
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { of as observableOf, BehaviorSubject, debounceTime, distinctUntilChanged, Observable, switchMap } from 'rxjs';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { ProductService } from '../../product/product.service';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { Bundle, BundleItem } from '../bundle';
|
||||
import { BundleService } from '../bundle.service';
|
||||
import { BundleDetailDatasource } from './bundle-detail-datasource';
|
||||
import { BundleDetailDialogComponent } from './bundle-detail-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-detail',
|
||||
templateUrl: './bundle-detail.component.html',
|
||||
styleUrls: ['./bundle-detail.component.css'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(BundleService);
|
||||
private productSer = inject(ProductService);
|
||||
|
||||
@ViewChild('name', { static: true }) nameElement?: ElementRef;
|
||||
|
||||
form: FormGroup<{
|
||||
// Bundle header fields
|
||||
name: FormControl<string>;
|
||||
units: FormControl<string>;
|
||||
salePrice: FormControl<number>;
|
||||
menuCategory: FormControl<string>;
|
||||
hasHappyHour: FormControl<boolean>;
|
||||
isNotAvailable: FormControl<boolean>;
|
||||
|
||||
// Item add row
|
||||
addRow: FormGroup<{
|
||||
itemId: FormControl<ProductQuery | string>;
|
||||
quantity: FormControl<number>;
|
||||
salePrice: FormControl<number>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
menuCategories: MenuCategory[] = [];
|
||||
public items$ = new BehaviorSubject<BundleItem[]>([]);
|
||||
dataSource: BundleDetailDatasource = new BundleDetailDatasource(this.items$);
|
||||
|
||||
item: Bundle = new Bundle();
|
||||
|
||||
itemProduct: ProductQuery | null = null;
|
||||
itemProducts: Observable<ProductQuery[]>;
|
||||
|
||||
displayedColumns = ['name', 'quantity', 'salePrice', 'action'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
units: new FormControl<string>('', { nonNullable: true }),
|
||||
salePrice: new FormControl<number>(0, { nonNullable: true }),
|
||||
menuCategory: new FormControl<string>('', { nonNullable: true }),
|
||||
hasHappyHour: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
isNotAvailable: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
|
||||
addRow: new FormGroup({
|
||||
itemId: new FormControl<ProductQuery | string>('', { nonNullable: true }),
|
||||
quantity: new FormControl<number>(1, { nonNullable: true }),
|
||||
salePrice: new FormControl<number>(0, { nonNullable: true }),
|
||||
}),
|
||||
});
|
||||
this.itemProducts = this.form.controls.addRow.controls.itemId.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => {
|
||||
// if user types, x is string; if user selects, x is Product
|
||||
if (typeof x !== 'string') {
|
||||
return observableOf([]);
|
||||
}
|
||||
return x.trim() === '' ? observableOf([]) : this.productSer.query(x);
|
||||
}),
|
||||
);
|
||||
this.form.controls.addRow.controls.itemId.valueChanges.subscribe((x) => {
|
||||
if (typeof x === 'string') {
|
||||
this.itemProduct = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as {
|
||||
item: Bundle;
|
||||
menuCategories: MenuCategory[];
|
||||
};
|
||||
this.menuCategories = data.menuCategories;
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
displayFn(product?: ProductQuery | string): string {
|
||||
return !product ? '' : typeof product === 'string' ? product : product.name;
|
||||
}
|
||||
|
||||
bundleSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const product = event.option.value as ProductQuery;
|
||||
this.itemProduct = product;
|
||||
this.form.controls.addRow.controls.salePrice.setValue(product.price ?? 0);
|
||||
}
|
||||
|
||||
private recalcBundleSalePrice(): void {
|
||||
const total = (this.item.items ?? []).reduce((sum, x) => {
|
||||
const qty = Number(x.quantity ?? 0);
|
||||
const price = Number(x.salePrice ?? 0);
|
||||
if (Number.isNaN(qty) || Number.isNaN(price)) return sum;
|
||||
return sum + qty * price;
|
||||
}, 0);
|
||||
|
||||
// keep model + form in sync
|
||||
this.item.salePrice = Number(total.toFixed(2));
|
||||
|
||||
// if the control is disabled, use patchValue
|
||||
this.form.controls.salePrice.patchValue(this.item.salePrice);
|
||||
}
|
||||
|
||||
showItem(item: Bundle) {
|
||||
this.item = item;
|
||||
|
||||
this.form.setValue({
|
||||
name: this.item.name ?? '',
|
||||
units: this.item.units ?? '',
|
||||
salePrice: Number(this.item.salePrice ?? 0),
|
||||
menuCategory: this.item.menuCategory?.id ?? '',
|
||||
hasHappyHour: this.item.hasHappyHour ?? false,
|
||||
isNotAvailable: this.item.isNotAvailable ?? false,
|
||||
|
||||
addRow: {
|
||||
itemId: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
},
|
||||
});
|
||||
this.itemProduct = null;
|
||||
this.form.controls.salePrice.disable({ emitEvent: false });
|
||||
|
||||
this.items$.next(this.item.items ?? []);
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
if (this.nameElement !== undefined) {
|
||||
this.nameElement.nativeElement.focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
resetAddRow() {
|
||||
this.form.controls.addRow.reset();
|
||||
this.itemProduct = null;
|
||||
}
|
||||
|
||||
addRow() {
|
||||
const v = this.form.value.addRow;
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.itemProduct?.id) {
|
||||
this.snackBar.open('Please select a product', 'Error');
|
||||
return;
|
||||
}
|
||||
|
||||
const quantity = Number(v.quantity ?? 0);
|
||||
if (Number.isNaN(quantity) || quantity <= 0) {
|
||||
this.snackBar.open('Quantity has to be > 0', 'Error');
|
||||
return;
|
||||
}
|
||||
|
||||
const salePrice = Number(v.salePrice ?? 0);
|
||||
if (Number.isNaN(salePrice) || salePrice < 0) {
|
||||
this.snackBar.open('Sale Price has to be >= 0', 'Error');
|
||||
return;
|
||||
}
|
||||
|
||||
// name is expected from backend as "name (units)" on GET;
|
||||
// for new rows we keep it blank (or you can set placeholder)
|
||||
const bi = new BundleItem({
|
||||
itemId: this.itemProduct?.id,
|
||||
name: this.itemProduct?.name ?? '',
|
||||
quantity,
|
||||
salePrice,
|
||||
});
|
||||
|
||||
this.item.items.push(bi);
|
||||
this.items$.next(this.item.items);
|
||||
this.resetAddRow();
|
||||
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
|
||||
editRow(row: BundleItem) {
|
||||
const dialogRef = this.dialog.open(BundleDetailDialogComponent, {
|
||||
width: '650px',
|
||||
data: {
|
||||
item: JSON.parse(JSON.stringify(row)) as BundleItem,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean | BundleItem) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
Object.assign(row, result as BundleItem);
|
||||
this.items$.next(this.item.items);
|
||||
this.resetAddRow();
|
||||
this.recalcBundleSalePrice();
|
||||
});
|
||||
}
|
||||
|
||||
deleteRow(row: BundleItem) {
|
||||
this.item.items.splice(this.item.items.indexOf(row), 1);
|
||||
this.items$.next(this.item.items);
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.item.items || this.item.items.length === 0) {
|
||||
this.snackBar.open('Bundle must contain at least one item', 'Error');
|
||||
return;
|
||||
}
|
||||
this.ser.saveOrUpdate(this.getItem()).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/bundles');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/bundles');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
confirmDelete(): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '250px',
|
||||
data: { title: 'Delete Bundle?', content: 'Are you sure? This cannot be undone.' },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
this.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getItem(): Bundle {
|
||||
const v = this.form.value;
|
||||
|
||||
this.item.name = v.name ?? '';
|
||||
this.item.units = v.units ?? '';
|
||||
this.item.salePrice = Number(v.salePrice ?? 0);
|
||||
this.item.hasHappyHour = v.hasHappyHour ?? false;
|
||||
this.item.isNotAvailable = v.isNotAvailable ?? false;
|
||||
this.item.sortOrder = this.item.sortOrder ?? 0;
|
||||
|
||||
// menu category
|
||||
const menuCategoryId = v.menuCategory ?? '';
|
||||
if (!menuCategoryId) {
|
||||
// keep it as-is; backend will 422 anyway, but we can show UI error too
|
||||
this.snackBar.open('Menu Category is required', 'Error');
|
||||
return this.item;
|
||||
}
|
||||
|
||||
if (this.item.menuCategory === null || this.item.menuCategory === undefined) {
|
||||
this.item.menuCategory = new MenuCategory();
|
||||
}
|
||||
this.item.menuCategory.id = menuCategoryId;
|
||||
|
||||
// ensure items array exists
|
||||
if (!this.item.items) {
|
||||
this.item.items = [];
|
||||
}
|
||||
|
||||
return this.item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleListResolver } from './bundle-list-resolver.service';
|
||||
|
||||
describe('BundleListResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleListResolver], (service: BundleListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Bundle } from './bundle';
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
export const bundleListResolver: ResolveFn<Bundle[]> = () => {
|
||||
return inject(BundleService).list();
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { Bundle } from '../bundle';
|
||||
|
||||
export class BundleListDataSource extends DataSource<Bundle> {
|
||||
public data: Bundle[];
|
||||
public filteredData: Bundle[];
|
||||
public search: string;
|
||||
public menuCategory: string;
|
||||
|
||||
constructor(
|
||||
private readonly searchFilter: Observable<string>,
|
||||
private readonly menuCategoryFilter: Observable<string>,
|
||||
private readonly dataObs: Observable<Bundle[]>,
|
||||
) {
|
||||
super();
|
||||
this.data = [];
|
||||
this.filteredData = [];
|
||||
this.search = '';
|
||||
this.menuCategory = '';
|
||||
}
|
||||
|
||||
connect(): Observable<Bundle[]> {
|
||||
const dataMutations = [
|
||||
this.dataObs.pipe(
|
||||
tap((x) => {
|
||||
this.data = x;
|
||||
}),
|
||||
),
|
||||
this.searchFilter.pipe(
|
||||
tap((x) => {
|
||||
this.search = x;
|
||||
}),
|
||||
),
|
||||
this.menuCategoryFilter.pipe(
|
||||
tap((x) => {
|
||||
this.menuCategory = x;
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
return merge(...dataMutations).pipe(
|
||||
map(() => this.getFilteredData(this.data, this.search, this.menuCategory)),
|
||||
tap((x) => {
|
||||
this.filteredData = x;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getFilteredData(data: Bundle[], search: string, menuCategory: string): Bundle[] {
|
||||
const tokens = (search ?? '').toLowerCase().split(/\s+/).filter(Boolean);
|
||||
|
||||
return data.filter((b: Bundle) => {
|
||||
const mcName = b.menuCategory?.name ?? '';
|
||||
const hay = `${b.name ?? ''} ${b.units ?? ''} ${mcName}`.toLowerCase();
|
||||
|
||||
const matchesSearch = tokens.length === 0 || tokens.every((t) => hay.includes(t));
|
||||
const matchesMenuCategory = menuCategory === '' || (b.menuCategory?.id ?? '') === menuCategory;
|
||||
|
||||
return matchesSearch && matchesMenuCategory;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mat-column-name {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mat-column-price,
|
||||
.mat-column-menuCategory,
|
||||
.mat-column-saleCategory,
|
||||
.mat-column-info {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mat-column-quantity {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Bundles</span>
|
||||
<button mat-icon-button (click)="exportCsv()" [disabled]="!(data | async)?.length">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<a mat-button [routerLink]="['/bundles', 'new']">
|
||||
<mat-icon>add_box</mat-icon>
|
||||
Add
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input type="text" matInput formControlName="filter" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select formControlName="menuCategory" (selectionChange)="filterOn($event.value)">
|
||||
<mat-option>-- All Bundles --</mat-option>
|
||||
@for (mc of menuCategories; track mc) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Bundles">
|
||||
<!-- Name -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<a [routerLink]="['/bundles', row.id]">{{ row.name }} ({{ row.units }})</a>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Price -->
|
||||
<ng-container matColumnDef="price">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Price</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">
|
||||
{{ row.salePrice | currency: 'INR' }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Menu Category -->
|
||||
<ng-container matColumnDef="menuCategory">
|
||||
<mat-header-cell *matHeaderCellDef>Menu Category</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
{{ row.menuCategory?.name }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Items -->
|
||||
<ng-container matColumnDef="items">
|
||||
<mat-header-cell *matHeaderCellDef>Items</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<div class="items-list">
|
||||
@for (item of row.items; track item) {
|
||||
<div class="item-name">{{ item.name }} x {{ item.quantity }} @ {{ item.salePrice | currency: 'INR' }}</div>
|
||||
}
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Info -->
|
||||
<ng-container matColumnDef="info">
|
||||
<mat-header-cell *matHeaderCellDef>Details</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<div class="row-container">
|
||||
<div>
|
||||
<mat-icon>
|
||||
{{ row.hasHappyHour ? 'sentiment_satisfied_alt' : 'sentiment_dissatisfied' }}
|
||||
</mat-icon>
|
||||
<b>{{ row.hasHappyHour ? 'Happy Hour' : 'Regular' }}</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-icon>
|
||||
{{ row.isNotAvailable ? 'pause' : 'play_arrow' }}
|
||||
</mat-icon>
|
||||
<b>{{ row.isNotAvailable ? 'Not Available' : 'Available' }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleListComponent } from './bundle-list.component';
|
||||
|
||||
describe('BundleListComponent', () => {
|
||||
let component: BundleListComponent;
|
||||
let fixture: ComponentFixture<BundleListComponent>;
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [BundleListComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BundleListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should compile', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
import { ToCsvService } from '../../shared/to-csv.service';
|
||||
import { Bundle } from '../bundle';
|
||||
import { BundleListDataSource } from './bundle-list-datasource';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-list',
|
||||
templateUrl: './bundle-list.component.html',
|
||||
styleUrls: ['./bundle-list.component.css'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class BundleListComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private toCsv = inject(ToCsvService);
|
||||
|
||||
searchFilter = new BehaviorSubject<string>('');
|
||||
menuCategoryFilter = new BehaviorSubject<string>('');
|
||||
data: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||
dataSource: BundleListDataSource = new BundleListDataSource(this.searchFilter, this.menuCategoryFilter, this.data);
|
||||
|
||||
form: FormGroup<{
|
||||
menuCategory: FormControl<string | null>;
|
||||
filter: FormControl<string>;
|
||||
}>;
|
||||
|
||||
list: Bundle[] = [];
|
||||
menuCategories: MenuCategory[] = [];
|
||||
displayedColumns: string[] = ['name', 'price', 'menuCategory', 'info', 'items'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
menuCategory: new FormControl<string | null>(''),
|
||||
filter: new FormControl<string>('', { nonNullable: true }),
|
||||
});
|
||||
|
||||
this.data.subscribe((data: Bundle[]) => {
|
||||
this.list = data;
|
||||
});
|
||||
|
||||
this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged()).subscribe((value) => {
|
||||
this.searchFilter.next(value ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
filterOn(val: string) {
|
||||
this.menuCategoryFilter.next(val);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource = new BundleListDataSource(this.searchFilter, this.menuCategoryFilter, this.data);
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Bundle[]; menuCategories: MenuCategory[] };
|
||||
this.loadData(data.list, data.menuCategories);
|
||||
});
|
||||
}
|
||||
|
||||
loadData(list: Bundle[], menuCategories: MenuCategory[]) {
|
||||
this.menuCategories = menuCategories;
|
||||
this.data.next(list);
|
||||
}
|
||||
|
||||
exportCsv() {
|
||||
const headers = {
|
||||
Name: 'name',
|
||||
Units: 'units',
|
||||
Price: 'salePrice',
|
||||
MenuCategory: 'menuCategory',
|
||||
Items: 'items',
|
||||
};
|
||||
|
||||
// Map items to count for csv (simple)
|
||||
const csvList = this.dataSource.filteredData.map((b) => ({
|
||||
...b,
|
||||
menuCategory: b.menuCategory?.name ?? '',
|
||||
items: (b.items ?? [])
|
||||
.map((i) => `${i.name}${i.quantity && i.quantity !== 1 ? ` x${i.quantity}` : ''}`)
|
||||
.join(' | '),
|
||||
}));
|
||||
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, csvList as unknown as Bundle[])], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(csvData);
|
||||
link.setAttribute('download', 'bundles.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleResolver } from './bundle-resolver.service';
|
||||
|
||||
describe('BundleResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleResolver], (service: BundleResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
|
||||
|
||||
import { Bundle } from './bundle';
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
export const bundleResolver: ResolveFn<Bundle> = (route: ActivatedRouteSnapshot) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(BundleService).get(id);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
describe('BundleService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleService],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleService], (service: BundleService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { Bundle } from './bundle';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
||||
};
|
||||
|
||||
const url = '/api/bundles';
|
||||
const serviceName = 'BundleService';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BundleService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<Bundle[]> {
|
||||
return this.http
|
||||
.get<Bundle[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Bundle[]>;
|
||||
}
|
||||
|
||||
get(id: string | null): Observable<Bundle> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Bundle>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Bundle>;
|
||||
}
|
||||
|
||||
save(bundle: Bundle): Observable<void> {
|
||||
return this.http
|
||||
.post<Bundle>(`${url}`, bundle, httpOptions)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<void>;
|
||||
}
|
||||
|
||||
update(bundle: Bundle): Observable<void> {
|
||||
return this.http
|
||||
.put<Bundle>(`${url}/${bundle.id}`, bundle, httpOptions)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<void>;
|
||||
}
|
||||
|
||||
saveOrUpdate(bundle: Bundle): Observable<void> {
|
||||
if (!bundle.versionId) {
|
||||
return this.save(bundle);
|
||||
}
|
||||
return this.update(bundle);
|
||||
}
|
||||
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http
|
||||
.delete<Bundle>(`${url}/${id}`, httpOptions)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<void>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { MenuCategory } from '../core/menu-category';
|
||||
|
||||
export class BundleItem {
|
||||
id?: string;
|
||||
itemId: string;
|
||||
name: string;
|
||||
salePrice: number;
|
||||
quantity: number;
|
||||
|
||||
public constructor(init?: Partial<BundleItem>) {
|
||||
this.itemId = '';
|
||||
this.name = '';
|
||||
this.salePrice = 0;
|
||||
this.quantity = 0;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
export class Bundle {
|
||||
// backend: id_ (sku id), version_id (skuVersion id)
|
||||
id?: string;
|
||||
versionId?: string;
|
||||
|
||||
name: string;
|
||||
units: string;
|
||||
salePrice: number;
|
||||
|
||||
hasHappyHour: boolean;
|
||||
isNotAvailable: boolean;
|
||||
sortOrder: number;
|
||||
|
||||
menuCategory?: MenuCategory | null;
|
||||
items: BundleItem[];
|
||||
|
||||
public constructor(init?: Partial<Bundle>) {
|
||||
this.name = '';
|
||||
this.units = '';
|
||||
this.salePrice = 0;
|
||||
|
||||
this.hasHappyHour = false;
|
||||
this.isNotAvailable = false;
|
||||
this.sortOrder = 0;
|
||||
|
||||
this.menuCategory = null;
|
||||
this.items = [];
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { menuCategoryListResolver } from '../menu-category/menu-category-list.resolver';
|
||||
import { BundleDetailComponent } from './bundle-detail/bundle-detail.component';
|
||||
import { bundleListResolver } from './bundle-list.resolver';
|
||||
import { BundleListComponent } from './bundle-list/bundle-list.component';
|
||||
import { bundleResolver } from './bundle.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: BundleListComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' }, // change if you have a separate permission
|
||||
resolve: {
|
||||
list: bundleListResolver,
|
||||
menuCategories: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: BundleDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' },
|
||||
resolve: {
|
||||
item: bundleResolver, // returns new Bundle() because id === null (no param here)
|
||||
menuCategories: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: BundleDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' },
|
||||
resolve: {
|
||||
item: bundleResolver,
|
||||
menuCategories: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in new issue
Block a user