Bundle item has an option to not print in the bill.

Bills and Kots should print properly with bundles
This commit is contained in:
2026-02-10 18:00:03 +00:00
parent 7382d487ac
commit 91b3740ec0
36 changed files with 290 additions and 141 deletions

View File

@ -22,6 +22,7 @@
<mat-label>Sale Price</mat-label>
<input matInput type="number" formControlName="salePrice" />
</mat-form-field>
<mat-checkbox formControlName="printInBill" class="flex-auto">Print in Bill?</mat-checkbox>
</div>
</form>
</div>

View File

@ -1,8 +1,9 @@
import { AsyncPipe } from '@angular/common';
import { CommonModule } 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 { 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';
@ -18,9 +19,10 @@ import { BundleItem } from '../bundle';
styleUrls: ['./bundle-detail-dialog.component.css'],
standalone: true,
imports: [
AsyncPipe,
CommonModule,
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
@ -40,6 +42,7 @@ export class BundleDetailDialogComponent implements OnInit {
product: FormControl<ProductQuery | string>;
quantity: FormControl<number>;
salePrice: FormControl<number>;
printInBill: FormControl<boolean>;
}>;
constructor() {
@ -47,6 +50,7 @@ export class BundleDetailDialogComponent implements OnInit {
product: 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.products$ = this.form.controls.product.valueChanges.pipe(
@ -70,6 +74,7 @@ export class BundleDetailDialogComponent implements OnInit {
product: this.data.item.name ?? '',
quantity: Number(this.data.item.quantity ?? 1),
salePrice: Number(this.data.item.salePrice ?? 0),
printInBill: this.data.item.printInBill,
});
this.selectedProduct = null;
@ -104,6 +109,7 @@ export class BundleDetailDialogComponent implements OnInit {
this.data.item.quantity = quantity;
this.data.item.salePrice = salePrice;
this.data.item.printInBill = v.printInBill || false;
this.dialogRef.close(this.data.item);
}

View File

@ -14,13 +14,13 @@
</div>
<div class="row-container">
<mat-form-field class="basis-1-4">
<mat-form-field class="flex-auto">
<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-form-field class="flex-auto">
<mat-label>Menu Category</mat-label>
<mat-select formControlName="menuCategory">
@for (mc of menuCategories; track mc) {
@ -31,8 +31,26 @@
</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>
<mat-form-field class="flex-auto">
<mat-label>Sale Category</mat-label>
<mat-select formControlName="saleCategory">
@for (sc of saleCategories; track sc) {
<mat-option [value]="sc.id">
{{ sc.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="row-container">
<span class="flex-auto"
>Sale Category is used for Tax Report and displaying in the right bill regimes. For the Sale Report, Discount
Report and Beer Sale Report, the Sale Category from the Bundle Items is used.</span
>
</div>
<div class="row-container">
<mat-checkbox formControlName="hasHappyHour" class="flex-auto">Has Happy Hour?</mat-checkbox>
<mat-checkbox formControlName="isNotAvailable" class="flex-auto">Not Available?</mat-checkbox>
</div>
<h2>Items</h2>
@ -63,6 +81,7 @@
<mat-label>Sale Price</mat-label>
<input matInput type="number" formControlName="salePrice" />
</mat-form-field>
<mat-checkbox formControlName="printInBill" class="basis-1-4">Print in Bill?</mat-checkbox>
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button>
</div>
@ -94,6 +113,16 @@
</mat-cell>
</ng-container>
<!-- Sale Price Column -->
<ng-container matColumnDef="printInBill">
<mat-header-cell *matHeaderCellDef class="right">Print in Bill</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">
<mat-icon>
{{ row.printInBill ? 'visibility' : 'visibility_off' }}
</mat-icon>
</mat-cell>
</ng-container>
<!-- Action Column -->
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>

View File

@ -17,6 +17,7 @@ import { of as observableOf, BehaviorSubject, debounceTime, distinctUntilChanged
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';
@ -59,6 +60,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
units: FormControl<string>;
salePrice: FormControl<number>;
menuCategory: FormControl<string>;
saleCategory: FormControl<string>;
hasHappyHour: FormControl<boolean>;
isNotAvailable: FormControl<boolean>;
@ -67,10 +69,12 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
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$);
@ -79,7 +83,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
itemProduct: ProductQuery | null = null;
itemProducts: Observable<ProductQuery[]>;
displayedColumns = ['name', 'quantity', 'salePrice', 'action'];
displayedColumns = ['name', 'quantity', 'salePrice', 'printInBill', 'action'];
constructor() {
this.form = new FormGroup({
@ -87,6 +91,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
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 }),
@ -94,6 +99,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
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(
@ -119,8 +125,10 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
const data = value as {
item: Bundle;
menuCategories: MenuCategory[];
saleCategories: SaleCategory[];
};
this.menuCategories = data.menuCategories;
this.saleCategories = data.saleCategories;
this.showItem(data.item);
});
}
@ -158,6 +166,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
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,
@ -165,6 +174,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
itemId: '',
quantity: 1,
salePrice: 0,
printInBill: true,
},
});
this.itemProduct = null;
@ -217,6 +227,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
name: this.itemProduct?.name ?? '',
quantity,
salePrice,
printInBill: v.printInBill || false,
});
this.item.items.push(bi);
@ -315,6 +326,19 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
}
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 = [];

View File

@ -55,12 +55,23 @@
</mat-cell>
</ng-container>
<!-- Sale Category -->
<ng-container matColumnDef="saleCategory">
<mat-header-cell *matHeaderCellDef>Sale Category</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row.saleCategory?.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) {
<mat-icon>
{{ item.printInBill ? 'visibility' : 'visibility_off' }}
</mat-icon>
<div class="item-name">{{ item.name }} x {{ item.quantity }} @ {{ item.salePrice | currency: 'INR' }}</div>
}
</div>

View File

@ -51,7 +51,7 @@ export class BundleListComponent implements OnInit {
list: Bundle[] = [];
menuCategories: MenuCategory[] = [];
displayedColumns: string[] = ['name', 'price', 'menuCategory', 'info', 'items'];
displayedColumns: string[] = ['name', 'price', 'menuCategory', 'saleCategory', 'info', 'items'];
constructor() {
this.form = new FormGroup({
@ -91,6 +91,7 @@ export class BundleListComponent implements OnInit {
Units: 'units',
Price: 'salePrice',
MenuCategory: 'menuCategory',
SaleCategory: 'saleCategory',
Items: 'items',
};

View File

@ -1,4 +1,5 @@
import { MenuCategory } from '../core/menu-category';
import { SaleCategory } from '../core/sale-category';
export class BundleItem {
id?: string;
@ -6,12 +7,14 @@ export class BundleItem {
name: string;
salePrice: number;
quantity: number;
printInBill: boolean;
public constructor(init?: Partial<BundleItem>) {
this.itemId = '';
this.name = '';
this.salePrice = 0;
this.quantity = 0;
this.printInBill = true;
Object.assign(this, init);
}
}
@ -30,6 +33,7 @@ export class Bundle {
sortOrder: number;
menuCategory?: MenuCategory | null;
saleCategory?: SaleCategory | null;
items: BundleItem[];
public constructor(init?: Partial<Bundle>) {

View File

@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { menuCategoryListResolver } from '../menu-category/menu-category-list.resolver';
import { saleCategoryListResolver } from '../sale-category/sale-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';
@ -26,6 +27,7 @@ export const routes: Routes = [
resolve: {
item: bundleResolver, // returns new Bundle() because id === null (no param here)
menuCategories: menuCategoryListResolver,
saleCategories: saleCategoryListResolver,
},
},
{
@ -36,6 +38,7 @@ export const routes: Routes = [
resolve: {
item: bundleResolver,
menuCategories: menuCategoryListResolver,
saleCategories: saleCategoryListResolver,
},
},
];

View File

@ -21,7 +21,6 @@ import { CustomerService } from '../customer.service';
styleUrls: ['./customer-detail.component.sass'],
imports: [
MatButtonModule,
MatCheckboxModule,
MatDividerModule,
MatFormFieldModule,