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"> <ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell> <mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>
<mat-cell *matCellDef="let row" class="center"> <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> <mat-icon>edit</mat-icon>
</button> --> </button>
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)"> <button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>

View File

@ -20,6 +20,7 @@ import { SaleCategory } from '../../core/sale-category';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ProductService } from '../product.service'; import { ProductService } from '../product.service';
import { ProductDetailDatasource } from './product-detail-datasource'; import { ProductDetailDatasource } from './product-detail-datasource';
import { ProductDetailDialogComponent } from './product-detail-dialog.component';
@Component({ @Component({
selector: 'app-product-detail', selector: 'app-product-detail',
@ -207,23 +208,30 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
} }
editRow(row: StockKeepingUnit) { editRow(row: StockKeepingUnit) {
// const dialogRef = this.dialog.open(ProductDetailDialogComponent, { const dialogRef = this.dialog.open(ProductDetailDialogComponent, {
// width: '750px', width: '750px',
// data: { data: {
// item: { ...row }, item: JSON.parse(JSON.stringify(row)) as StockKeepingUnit,
// isSold: this.item.isSold, units: row.units,
// isPurchased: this.item.isPurchased, fraction: row.fraction,
// }, productYield: row.productYield,
// }); costPrice: row.costPrice,
// dialogRef.afterClosed().subscribe((result: boolean | StockKeepingUnit) => { salePrice: row.salePrice,
// if (!result) { menuCategory: row.menuCategory?.id ?? '',
// return; hasHappyHour: row.hasHappyHour,
// } isNotAvailable: row.isNotAvailable,
// const j = result as StockKeepingUnit; menuCategories: this.menuCategories,
// Object.assign(row, j); },
// this.skus.next(this.item.skus); });
// this.resetAddRow(); 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) { deleteRow(row: StockKeepingUnit) {

View File

@ -51,23 +51,28 @@ export class ProductListDataSource extends DataSource<Product> {
disconnect() {} disconnect() {}
private getFilteredData(data: Product[], search: string, menuCategory: string): Product[] { private getFilteredData(data: Product[], search: string, menuCategory: string): Product[] {
return data; const tokens = (search ?? '').toLowerCase().split(/\s+/).filter(Boolean);
// return data return data.filter((product: Product) => {
// .filter( search = search.toLowerCase();
// (x: Product) =>
// search === null || const skus = product.skus ?? [];
// search === undefined ||
// search === '' || // 1) Search: match ANY product/sku fields
// `${x.name} ${x.units} ${x.saleCategory?.name} ${x.menuCategory?.name}` const matchesSearch =
// .toLowerCase() tokens.length === 0 ||
// .indexOf(search.toLowerCase()) !== -1, tokens.every(
// ) (token) =>
// .filter( `${product.name ?? ''} ${product.fractionUnits ?? ''} ${product.saleCategory?.name ?? ''}`
// (x) => .toLowerCase()
// menuCategory === null || .includes(token) ||
// menuCategory === undefined || skus.some((k) => {
// menuCategory === '' || const hay = `${k.units ?? ''} ${k.menuCategory?.name ?? ''}`.toLowerCase();
// (x.menuCategory as MenuCategory).id === menuCategory, 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 { AsyncPipe, DecimalPipe, CurrencyPipe } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; 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 { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { MenuCategory } from '../../core/menu-category'; import { MenuCategory } from '../../core/menu-category';
@ -46,7 +46,7 @@ export class ProductListComponent implements OnInit {
private toCsv = inject(ToCsvService); private toCsv = inject(ToCsvService);
private ser = inject(ProductService); private ser = inject(ProductService);
searchFilter = new Observable<string>(); searchFilter = new BehaviorSubject<string>('');
menuCategoryFilter = new BehaviorSubject<string>(''); menuCategoryFilter = new BehaviorSubject<string>('');
data: BehaviorSubject<Product[]> = new BehaviorSubject<Product[]>([]); data: BehaviorSubject<Product[]> = new BehaviorSubject<Product[]>([]);
dataSource: ProductListDataSource = new ProductListDataSource(this.searchFilter, this.menuCategoryFilter, this.data); 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.data.subscribe((data: Product[]) => {
this.list = data; 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) { filterOn(val: string) {
@ -102,16 +110,16 @@ export class ProductListComponent implements OnInit {
} }
dropTable(event: CdkDragDrop<ProductListDataSource>) { dropTable(event: CdkDragDrop<ProductListDataSource>) {
// const prevIndex = this.dataSource.filteredData.indexOf(event.item.data); const prevIndex = this.dataSource.filteredData.indexOf(event.item.data);
// moveItemInArray(this.dataSource.filteredData, prevIndex, event.currentIndex); moveItemInArray(this.dataSource.filteredData, prevIndex, event.currentIndex);
// if (this.dataSource.menuCategory === undefined) { if (this.dataSource.menuCategory === undefined) {
// this.list = this.dataSource.filteredData; this.list = this.dataSource.filteredData;
// } else { } else {
// this.list = this.list this.list = this.list
// .filter((x) => (x.menuCategory as MenuCategory).id !== this.dataSource.menuCategory) .filter((x) => !x.skus.some((k) => k.menuCategory?.id === this.dataSource.menuCategory))
// .concat(this.dataSource.filteredData); .concat(this.dataSource.filteredData);
// } }
// this.data.next(this.list); this.data.next(this.list);
} }
exportCsv() { exportCsv() {