Product list and detail fully working. only thing to check is the update sort order route

This commit is contained in:
2026-01-26 14:01:18 +00:00
parent 22f888500f
commit 0a7ffb4a5c
8 changed files with 261 additions and 51 deletions

View File

@ -0,0 +1,50 @@
<h1 mat-dialog-title>Edit Product SKU</h1>
<div mat-dialog-content>
<form [formGroup]="form">
<div class="flex flex-row flex-wrap justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto">
<mat-label>Units</mat-label>
<input matInput formControlName="units" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Fraction</mat-label>
<input matInput type="number" formControlName="fraction" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Yield</mat-label>
<input matInput type="number" formControlName="productYield" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Cost Price</mat-label>
<input matInput type="number" formControlName="costPrice" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Sale Price</mat-label>
<input matInput type="number" formControlName="salePrice" />
</mat-form-field>
<mat-form-field class="flex-auto">
<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="flex-auto">Has Happy Hour?</mat-checkbox>
<mat-checkbox formControlName="isNotAvailable" class="flex-auto">Not Available?</mat-checkbox>
</div>
</form>
</div>
<div mat-dialog-actions>
<button mat-raised-button color="warn" [mat-dialog-close]="false" cdkFocusInitial>Cancel</button>
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
</div>

View File

@ -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<ProductDetailDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductDetailDialogComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProductDetailDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<ProductDetailDialogComponent>>(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<string>;
fraction: FormControl<number>;
productYield: FormControl<number>;
costPrice: FormControl<number>;
salePrice: FormControl<number>;
menuCategory: FormControl<string>;
hasHappyHour: FormControl<boolean>;
isNotAvailable: FormControl<boolean>;
}>;
constructor() {
this.form = new FormGroup({
units: new FormControl<string>('', { nonNullable: true }),
fraction: new FormControl<number>(1, { nonNullable: true }),
productYield: new FormControl<number>(1, { nonNullable: true }),
costPrice: new FormControl<number>(0, { 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 }),
});
}
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);
}
}

View File

@ -130,9 +130,9 @@
<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)">
<button mat-icon-button tabindex="-1" (click)="editRow(row)">
<mat-icon>edit</mat-icon>
</button> -->
</button>
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
<mat-icon>delete</mat-icon>
</button>

View File

@ -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) {

View File

@ -51,23 +51,28 @@ export class ProductListDataSource extends DataSource<Product> {
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;
});
}
}

View File

@ -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<string>();
searchFilter = new BehaviorSubject<string>('');
menuCategoryFilter = new BehaviorSubject<string>('');
data: BehaviorSubject<Product[]> = new BehaviorSubject<Product[]>([]);
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<ProductListDataSource>) {
// 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() {