350 lines
11 KiB
TypeScript
350 lines
11 KiB
TypeScript
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 { SaleCategory } from '../../core/sale-category';
|
|
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>;
|
|
saleCategory: FormControl<string>;
|
|
hasHappyHour: FormControl<boolean>;
|
|
isNotAvailable: FormControl<boolean>;
|
|
|
|
// Item add row
|
|
addRow: FormGroup<{
|
|
itemId: FormControl<ProductQuery | string>;
|
|
quantity: FormControl<number>;
|
|
salePrice: FormControl<number>;
|
|
printInBill: FormControl<boolean>;
|
|
}>;
|
|
}>;
|
|
|
|
menuCategories: MenuCategory[] = [];
|
|
saleCategories: SaleCategory[] = [];
|
|
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', 'printInBill', '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 }),
|
|
saleCategory: 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 }),
|
|
printInBill: new FormControl<boolean>(true, { 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[];
|
|
saleCategories: SaleCategory[];
|
|
};
|
|
this.menuCategories = data.menuCategories;
|
|
this.saleCategories = data.saleCategories;
|
|
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 ?? '',
|
|
saleCategory: this.item.saleCategory?.id ?? '',
|
|
hasHappyHour: this.item.hasHappyHour ?? false,
|
|
isNotAvailable: this.item.isNotAvailable ?? false,
|
|
|
|
addRow: {
|
|
itemId: '',
|
|
quantity: 1,
|
|
salePrice: 0,
|
|
printInBill: true,
|
|
},
|
|
});
|
|
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,
|
|
printInBill: v.printInBill || false,
|
|
});
|
|
|
|
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;
|
|
|
|
// sale category
|
|
const saleCategoryId = v.saleCategory ?? '';
|
|
if (!saleCategoryId) {
|
|
// 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.saleCategory === null || this.item.saleCategory === undefined) {
|
|
this.item.saleCategory = new SaleCategory();
|
|
}
|
|
this.item.saleCategory.id = saleCategoryId;
|
|
|
|
// ensure items array exists
|
|
if (!this.item.items) {
|
|
this.item.items = [];
|
|
}
|
|
|
|
return this.item;
|
|
}
|
|
}
|