diff --git a/bookie/src/app/product/product-detail/product-detail-dialog.component.css b/bookie/src/app/product/product-detail/product-detail-dialog.component.css new file mode 100644 index 00000000..e69de29b diff --git a/bookie/src/app/product/product-detail/product-detail-dialog.component.html b/bookie/src/app/product/product-detail/product-detail-dialog.component.html new file mode 100644 index 00000000..3efa9c22 --- /dev/null +++ b/bookie/src/app/product/product-detail/product-detail-dialog.component.html @@ -0,0 +1,50 @@ +

Edit Product SKU

+
+
+
+ + Units + + + + + Fraction + + + + + Yield + + + + + Cost Price + + + + + Sale Price + + + + + Menu Category + + @for (mc of menuCategories; track mc) { + + {{ mc.name }} + + } + + + + Has Happy Hour? + Not Available? +
+
+
+ +
+ + +
diff --git a/bookie/src/app/product/product-detail/product-detail-dialog.component.spec.ts b/bookie/src/app/product/product-detail/product-detail-dialog.component.spec.ts new file mode 100644 index 00000000..401e44da --- /dev/null +++ b/bookie/src/app/product/product-detail/product-detail-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProductDetailDialogComponent } from './product-detail-dialog.component'; + +describe('ProductDetailDialogComponent', () => { + let component: ProductDetailDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProductDetailDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ProductDetailDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product/product-detail/product-detail-dialog.component.ts b/bookie/src/app/product/product-detail/product-detail-dialog.component.ts new file mode 100644 index 00000000..e22a2cf8 --- /dev/null +++ b/bookie/src/app/product/product-detail/product-detail-dialog.component.ts @@ -0,0 +1,117 @@ +import { CdkScrollableModule } from '@angular/cdk/scrolling'; +import { Component, inject, OnInit } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MenuCategory } from 'src/app/core/menu-category'; + +import { StockKeepingUnit } from '../../core/product'; + +@Component({ + selector: 'app-product-detail-dialog', + templateUrl: './product-detail-dialog.component.html', + styleUrls: ['./product-detail-dialog.component.css'], + standalone: true, + imports: [ + CdkScrollableModule, + MatButtonModule, + MatCheckboxModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + ReactiveFormsModule, + ], +}) +export class ProductDetailDialogComponent implements OnInit { + private readonly dialogRef = inject>(MatDialogRef); + menuCategories: MenuCategory[] = []; + data = inject<{ + item: StockKeepingUnit; + units: string; + fraction: number; + productYield: number; + costPrice: number; + salePrice: number; + menuCategory: string; + hasHappyHour: boolean; + isNotAvailable: boolean; + menuCategories: MenuCategory[]; + }>(MAT_DIALOG_DATA); + + form: FormGroup<{ + units: FormControl; + fraction: FormControl; + productYield: FormControl; + costPrice: FormControl; + salePrice: FormControl; + menuCategory: FormControl; + hasHappyHour: FormControl; + isNotAvailable: FormControl; + }>; + + constructor() { + this.form = new FormGroup({ + units: new FormControl('', { nonNullable: true }), + fraction: new FormControl(1, { nonNullable: true }), + productYield: new FormControl(1, { nonNullable: true }), + costPrice: new FormControl(0, { nonNullable: true }), + salePrice: new FormControl(0, { nonNullable: true }), + menuCategory: new FormControl('', { nonNullable: true }), + hasHappyHour: new FormControl(false, { nonNullable: true }), + isNotAvailable: new FormControl(false, { nonNullable: true }), + }); + } + + ngOnInit(): void { + // Populate from the row being edited + this.form.setValue({ + units: this.data.item.units ?? '', + fraction: this.data.item.fraction ?? 1, + productYield: this.data.item.productYield ?? 1, + costPrice: this.data.item.costPrice ?? 0, + salePrice: this.data.item.salePrice ?? 0, + menuCategory: this.data.item.menuCategory?.id ?? '', + hasHappyHour: this.data.item.hasHappyHour ?? false, + isNotAvailable: this.data.item.isNotAvailable ?? false, + }); + this.menuCategories = this.data.menuCategories; + } + + accept(): void { + const formValue = this.form.value; + const fraction = formValue.fraction ?? 0; + if (fraction < 1) { + return; + } + const productYield = formValue.productYield ?? 0; + if (productYield < 0 || productYield > 1) { + return; + } + const costPrice = formValue.costPrice ?? 0; + if (costPrice < 0) { + return; + } + const salePrice = formValue.salePrice ?? 0; + if (salePrice < 0) { + return; + } + this.data.item.units = (formValue.units ?? '').trim(); + this.data.item.fraction = fraction; + this.data.item.productYield = productYield; + this.data.item.costPrice = costPrice; + this.data.item.salePrice = salePrice; + this.data.item.hasHappyHour = formValue.hasHappyHour ?? false; + this.data.item.isNotAvailable = formValue.isNotAvailable ?? false; + if (this.data.item.menuCategory === null || this.data.item.menuCategory === undefined) { + this.data.item.menuCategory = new MenuCategory(); + } + this.data.item.menuCategory.id = formValue.menuCategory ?? ''; + this.data.item.menuCategory.name = this.menuCategories.find((mc) => mc.id === formValue.menuCategory)?.name ?? ''; + this.dialogRef.close(this.data.item); + } +} diff --git a/bookie/src/app/product/product-detail/product-detail.component.html b/bookie/src/app/product/product-detail/product-detail.component.html index 9deafb35..d4c96961 100644 --- a/bookie/src/app/product/product-detail/product-detail.component.html +++ b/bookie/src/app/product/product-detail/product-detail.component.html @@ -130,9 +130,9 @@ Action - + diff --git a/bookie/src/app/product/product-detail/product-detail.component.ts b/bookie/src/app/product/product-detail/product-detail.component.ts index a8d87d62..aafefa06 100644 --- a/bookie/src/app/product/product-detail/product-detail.component.ts +++ b/bookie/src/app/product/product-detail/product-detail.component.ts @@ -20,6 +20,7 @@ import { SaleCategory } from '../../core/sale-category'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ProductService } from '../product.service'; import { ProductDetailDatasource } from './product-detail-datasource'; +import { ProductDetailDialogComponent } from './product-detail-dialog.component'; @Component({ selector: 'app-product-detail', @@ -207,23 +208,30 @@ export class ProductDetailComponent implements OnInit, AfterViewInit { } editRow(row: StockKeepingUnit) { - // const dialogRef = this.dialog.open(ProductDetailDialogComponent, { - // width: '750px', - // data: { - // item: { ...row }, - // isSold: this.item.isSold, - // isPurchased: this.item.isPurchased, - // }, - // }); - // dialogRef.afterClosed().subscribe((result: boolean | StockKeepingUnit) => { - // if (!result) { - // return; - // } - // const j = result as StockKeepingUnit; - // Object.assign(row, j); - // this.skus.next(this.item.skus); - // this.resetAddRow(); - // }); + const dialogRef = this.dialog.open(ProductDetailDialogComponent, { + width: '750px', + data: { + item: JSON.parse(JSON.stringify(row)) as StockKeepingUnit, + units: row.units, + fraction: row.fraction, + productYield: row.productYield, + costPrice: row.costPrice, + salePrice: row.salePrice, + menuCategory: row.menuCategory?.id ?? '', + hasHappyHour: row.hasHappyHour, + isNotAvailable: row.isNotAvailable, + menuCategories: this.menuCategories, + }, + }); + dialogRef.afterClosed().subscribe((result: boolean | StockKeepingUnit) => { + if (!result) { + return; + } + const j = result as StockKeepingUnit; + Object.assign(row, j); + this.skus.next(this.item.skus); + this.resetAddRow(); + }); } deleteRow(row: StockKeepingUnit) { diff --git a/bookie/src/app/product/product-list/product-list-datasource.ts b/bookie/src/app/product/product-list/product-list-datasource.ts index f96b03d6..d75e9c01 100644 --- a/bookie/src/app/product/product-list/product-list-datasource.ts +++ b/bookie/src/app/product/product-list/product-list-datasource.ts @@ -51,23 +51,28 @@ export class ProductListDataSource extends DataSource { disconnect() {} private getFilteredData(data: Product[], search: string, menuCategory: string): Product[] { - return data; - // return data - // .filter( - // (x: Product) => - // search === null || - // search === undefined || - // search === '' || - // `${x.name} ${x.units} ${x.saleCategory?.name} ${x.menuCategory?.name}` - // .toLowerCase() - // .indexOf(search.toLowerCase()) !== -1, - // ) - // .filter( - // (x) => - // menuCategory === null || - // menuCategory === undefined || - // menuCategory === '' || - // (x.menuCategory as MenuCategory).id === menuCategory, - // ); + const tokens = (search ?? '').toLowerCase().split(/\s+/).filter(Boolean); + return data.filter((product: Product) => { + search = search.toLowerCase(); + + const skus = product.skus ?? []; + + // 1) Search: match ANY product/sku fields + const matchesSearch = + tokens.length === 0 || + tokens.every( + (token) => + `${product.name ?? ''} ${product.fractionUnits ?? ''} ${product.saleCategory?.name ?? ''}` + .toLowerCase() + .includes(token) || + skus.some((k) => { + const hay = `${k.units ?? ''} ${k.menuCategory?.name ?? ''}`.toLowerCase(); + return hay.includes(token); + }), + ); + + const matchesMenuCategory = menuCategory === '' || skus.some((k) => (k.menuCategory?.id ?? '') === menuCategory); + return matchesSearch && matchesMenuCategory; + }); } } diff --git a/bookie/src/app/product/product-list/product-list.component.ts b/bookie/src/app/product/product-list/product-list.component.ts index 8517dcc6..28aa31dc 100644 --- a/bookie/src/app/product/product-list/product-list.component.ts +++ b/bookie/src/app/product/product-list/product-list.component.ts @@ -1,4 +1,4 @@ -import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'; +import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; import { AsyncPipe, DecimalPipe, CurrencyPipe } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; @@ -11,7 +11,7 @@ import { MatSelectModule } from '@angular/material/select'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatTableModule } from '@angular/material/table'; import { ActivatedRoute, RouterLink } from '@angular/router'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { MenuCategory } from '../../core/menu-category'; @@ -46,7 +46,7 @@ export class ProductListComponent implements OnInit { private toCsv = inject(ToCsvService); private ser = inject(ProductService); - searchFilter = new Observable(); + searchFilter = new BehaviorSubject(''); menuCategoryFilter = new BehaviorSubject(''); data: BehaviorSubject = new BehaviorSubject([]); dataSource: ProductListDataSource = new ProductListDataSource(this.searchFilter, this.menuCategoryFilter, this.data); @@ -69,7 +69,15 @@ export class ProductListComponent implements OnInit { this.data.subscribe((data: Product[]) => { this.list = data; }); - this.searchFilter = this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged()); + this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged()).subscribe((value) => { + this.searchFilter.next(value ?? ''); + }); + this.menuCategoryFilter.subscribe((val) => { + console.log('Menu category filter changed to ', val); + }); + this.searchFilter.subscribe((val) => { + console.log('Search filter changed to ', val); + }); } filterOn(val: string) { @@ -102,16 +110,16 @@ export class ProductListComponent implements OnInit { } dropTable(event: CdkDragDrop) { - // const prevIndex = this.dataSource.filteredData.indexOf(event.item.data); - // moveItemInArray(this.dataSource.filteredData, prevIndex, event.currentIndex); - // if (this.dataSource.menuCategory === undefined) { - // this.list = this.dataSource.filteredData; - // } else { - // this.list = this.list - // .filter((x) => (x.menuCategory as MenuCategory).id !== this.dataSource.menuCategory) - // .concat(this.dataSource.filteredData); - // } - // this.data.next(this.list); + const prevIndex = this.dataSource.filteredData.indexOf(event.item.data); + moveItemInArray(this.dataSource.filteredData, prevIndex, event.currentIndex); + if (this.dataSource.menuCategory === undefined) { + this.list = this.dataSource.filteredData; + } else { + this.list = this.list + .filter((x) => !x.skus.some((k) => k.menuCategory?.id === this.dataSource.menuCategory)) + .concat(this.dataSource.filteredData); + } + this.data.next(this.list); } exportCsv() {