diff --git a/barker/barker/routers/customer.py b/barker/barker/routers/customer.py index 8275a01..f26303a 100644 --- a/barker/barker/routers/customer.py +++ b/barker/barker/routers/customer.py @@ -5,7 +5,7 @@ from typing import List import barker.schemas.customer as schemas from fastapi import APIRouter, Depends, HTTPException, Security, status -from sqlalchemy import or_, select +from sqlalchemy import delete, or_, select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session @@ -62,7 +62,7 @@ def update_route( ) -def add_discounts(customer: Customer, discounts: List[schemas.DiscountItem], db: Session): +def add_discounts(customer: Customer, discounts: List[schemas.DiscountItem], db: Session) -> None: for discount in discounts: cd = next((d for d in customer.discounts if d.sale_category_id == discount.id_), None) if cd is None: @@ -77,10 +77,10 @@ def add_discounts(customer: Customer, discounts: List[schemas.DiscountItem], db: def delete_route( id_: uuid.UUID, user: UserToken = Security(get_user, scopes=["customers"]), -): +) -> None: with SessionFuture() as db: - item: Customer = db.execute(select(Customer).where(Customer.id == id_)).scalar_one() - db.delete(item) + db.execute(delete(CustomerDiscount).where(CustomerDiscount.customer_id == id_)) + db.execute(delete(Customer).where(Customer.id == id_)) db.commit() @@ -88,7 +88,8 @@ def delete_route( def show_blank( user: UserToken = Security(get_user, scopes=["customers"]), ) -> schemas.CustomerBlank: - return blank_customer_info() + with SessionFuture() as db: + return blank_customer_info(db) @router.get("/list", response_model=List[schemas.Customer]) @@ -162,5 +163,18 @@ def customer_info_for_list(item: Customer) -> schemas.Customer: ) -def blank_customer_info() -> schemas.CustomerBlank: - return schemas.CustomerBlank(name="", address="", phone="", printInBill=False, discounts=[]) +def blank_customer_info(db: Session) -> schemas.CustomerBlank: + return schemas.CustomerBlank( + name="", + address="", + phone="", + printInBill=False, + discounts=[ + { + "id": sc.id, + "name": sc.name, + "discount": 0, + } + for sc in db.execute(select(SaleCategory).order_by(SaleCategory.name)).scalars().all() + ], + ) diff --git a/bookie/src/app/customers/customer-detail/customer-detail.component.css b/bookie/src/app/customers/customer-detail/customer-detail.component.css index 82c7afd..8442f47 100644 --- a/bookie/src/app/customers/customer-detail/customer-detail.component.css +++ b/bookie/src/app/customers/customer-detail/customer-detail.component.css @@ -1,3 +1,23 @@ .example-card { max-width: 400px; } +/*.discounts > div:nth-child(even) .mat-form-field {*/ +/* margin-left: 8px;*/ +/*}*/ + +/*.discounts > div:nth-child(odd) .mat-form-field {*/ +/* margin-right: 8px;*/ +/*}*/ + +.discounts > div:nth-child(3n + 1) .mat-form-field { + margin-right: 4px; +} + +.discounts > div:nth-child(3n + 2) .mat-form-field { + margin-left: 4px; + margin-right: 4px; +} + +.discounts > div:nth-child(3n) .mat-form-field { + margin-left: 4px; +} diff --git a/bookie/src/app/customers/customer-detail/customer-detail.component.html b/bookie/src/app/customers/customer-detail/customer-detail.component.html index 0f0b63c..f21e8a4 100644 --- a/bookie/src/app/customers/customer-detail/customer-detail.component.html +++ b/bookie/src/app/customers/customer-detail/customer-detail.component.html @@ -50,18 +50,16 @@ > Print in Bill? - - -
+

+
Discount on {{ r.name }} diff --git a/bookie/src/app/customers/customer.service.ts b/bookie/src/app/customers/customer.service.ts index 8718308..cea80c1 100644 --- a/bookie/src/app/customers/customer.service.ts +++ b/bookie/src/app/customers/customer.service.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; import { catchError } from 'rxjs/operators'; @@ -55,4 +55,13 @@ export class CustomerService { .delete(`${url}/${id}`, httpOptions) .pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable; } + + autocomplete(query: string): Observable { + const options = { params: new HttpParams().set('q', query) }; + return this.http + .get(`${url}/query`, options) + .pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable< + Customer[] + >; + } } diff --git a/bookie/src/app/guest-book/customer.ts b/bookie/src/app/guest-book/customer.ts deleted file mode 100644 index 08aad4d..0000000 --- a/bookie/src/app/guest-book/customer.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class Customer { - name: string; - phone: string; - address: string; - - public constructor(init?: Partial) { - this.name = ''; - this.phone = ''; - this.address = ''; - Object.assign(this, init); - } -} diff --git a/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.ts b/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.ts index 69dab0b..762e129 100644 --- a/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.ts +++ b/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.ts @@ -5,8 +5,9 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of as observableOf } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; +import { Customer } from '../../core/customer'; import { ToasterService } from '../../core/toaster.service'; -import { Customer } from '../customer'; +import { CustomerService } from '../../customers/customer.service'; import { GuestBook } from '../guest-book'; import { GuestBookService } from '../guest-book.service'; @@ -27,6 +28,7 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit { private router: Router, private toaster: ToasterService, private ser: GuestBookService, + private customerService: CustomerService, ) { // Create form this.form = this.fb.group({ @@ -41,7 +43,7 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit { map((x) => (x !== null && x.length >= 1 ? x : null)), debounceTime(150), distinctUntilChanged(), - switchMap((x) => (x === null ? observableOf([]) : this.ser.autocomplete(x))), + switchMap((x) => (x === null ? observableOf([]) : this.customerService.autocomplete(x))), ); } diff --git a/bookie/src/app/guest-book/guest-book.service.ts b/bookie/src/app/guest-book/guest-book.service.ts index 5da050a..a3971f7 100644 --- a/bookie/src/app/guest-book/guest-book.service.ts +++ b/bookie/src/app/guest-book/guest-book.service.ts @@ -5,7 +5,6 @@ import { catchError } from 'rxjs/operators'; import { ErrorLoggerService } from '../core/error-logger.service'; -import { Customer } from './customer'; import { GuestBook } from './guest-book'; import { GuestBookList } from './guest-book-list'; @@ -59,13 +58,4 @@ export class GuestBookService { .delete(`${url}/${id}`, httpOptions) .pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable; } - - autocomplete(query: string): Observable { - const options = { params: new HttpParams().set('q', query) }; - return this.http - .get(`${customerUrl}/query`, options) - .pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable< - Customer[] - >; - } } diff --git a/bookie/src/app/sales/bills/bills.component.html b/bookie/src/app/sales/bills/bills.component.html index 51facf9..546f200 100644 --- a/bookie/src/app/sales/bills/bills.component.html +++ b/bookie/src/app/sales/bills/bills.component.html @@ -45,7 +45,9 @@ / / - + {{ bs.bill.customer?.name || 'Customer' }} + diff --git a/bookie/src/app/sales/bills/bills.component.ts b/bookie/src/app/sales/bills/bills.component.ts index d578920..19f8765 100644 --- a/bookie/src/app/sales/bills/bills.component.ts +++ b/bookie/src/app/sales/bills/bills.component.ts @@ -6,11 +6,13 @@ import { map, switchMap } from 'rxjs/operators'; import { AuthService } from '../../auth/auth.service'; import { BillViewItem } from '../../core/bill-view-item'; +import { Customer } from '../../core/customer'; import { Table } from '../../core/table'; import { ToasterService } from '../../core/toaster.service'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { TableService } from '../../tables/table.service'; import { BillService } from '../bill.service'; +import { ChooseCustomerComponent } from '../choose-customer/choose-customer.component'; import { PaxComponent } from '../pax/pax.component'; import { QuantityComponent } from '../quantity/quantity.component'; import { TablesDialogComponent } from '../tables-dialog/tables-dialog.component'; @@ -72,6 +74,20 @@ export class BillsComponent implements OnInit { }); } + chooseCustomer() { + const dialogRef = this.dialog.open(ChooseCustomerComponent, { + // width: '750px', + data: this.bs.bill.customer?.id, + }); + + dialogRef.afterClosed().subscribe((result: boolean | Customer) => { + if (!result) { + return; + } + this.bs.bill.customer = result as Customer; + }); + } + isAllSelected(kotView: BillViewItem): boolean { const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot; return kot.inventories.reduce( diff --git a/bookie/src/app/sales/choose-customer/choose-customer.component.css b/bookie/src/app/sales/choose-customer/choose-customer.component.css new file mode 100644 index 0000000..8442f47 --- /dev/null +++ b/bookie/src/app/sales/choose-customer/choose-customer.component.css @@ -0,0 +1,23 @@ +.example-card { + max-width: 400px; +} +/*.discounts > div:nth-child(even) .mat-form-field {*/ +/* margin-left: 8px;*/ +/*}*/ + +/*.discounts > div:nth-child(odd) .mat-form-field {*/ +/* margin-right: 8px;*/ +/*}*/ + +.discounts > div:nth-child(3n + 1) .mat-form-field { + margin-right: 4px; +} + +.discounts > div:nth-child(3n + 2) .mat-form-field { + margin-left: 4px; + margin-right: 4px; +} + +.discounts > div:nth-child(3n) .mat-form-field { + margin-left: 4px; +} diff --git a/bookie/src/app/sales/choose-customer/choose-customer.component.html b/bookie/src/app/sales/choose-customer/choose-customer.component.html new file mode 100644 index 0000000..5882318 --- /dev/null +++ b/bookie/src/app/sales/choose-customer/choose-customer.component.html @@ -0,0 +1,90 @@ +

Customer

+ +
+
+ + Phone + + + + {{ customer.name }} - {{ customer.phone }} + +
+
+ + Name + + +
+
+ + Address + + +
+
+ Print in Bill? +
+

+
+
+ + Discount on {{ r.name }} + + % + +
+
+
+
+ + + + diff --git a/bookie/src/app/sales/choose-customer/choose-customer.component.spec.ts b/bookie/src/app/sales/choose-customer/choose-customer.component.spec.ts new file mode 100644 index 0000000..302e141 --- /dev/null +++ b/bookie/src/app/sales/choose-customer/choose-customer.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { ChooseCustomerComponent } from './choose-customer.component'; + +describe('ChooseCustomerComponent', () => { + let component: ChooseCustomerComponent; + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ChooseCustomerComponent], + }).compileComponents(); + }), + ); + + beforeEach(() => { + fixture = TestBed.createComponent(ChooseCustomerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/sales/choose-customer/choose-customer.component.ts b/bookie/src/app/sales/choose-customer/choose-customer.component.ts new file mode 100644 index 0000000..c5aee34 --- /dev/null +++ b/bookie/src/app/sales/choose-customer/choose-customer.component.ts @@ -0,0 +1,105 @@ +import { Component, Inject } from '@angular/core'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { round } from 'mathjs'; +import { Observable, of as observableOf } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; + +import { Customer } from '../../core/customer'; +import { CustomerService } from '../../customers/customer.service'; + +@Component({ + selector: 'app-choose-customer', + templateUrl: './choose-customer.component.html', + styleUrls: ['./choose-customer.component.css'], +}) +export class ChooseCustomerComponent { + form: FormGroup; + item: Customer = new Customer(); + customers: Observable; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: string | undefined, + private fb: FormBuilder, + private ser: CustomerService, + ) { + // Create form + this.form = this.fb.group({ + name: '', + phone: '', + address: '', + printInBill: false, + discounts: this.fb.array([]), + }); + // Setup Account Autocomplete + this.customers = (this.form.get('phone') as FormControl).valueChanges.pipe( + startWith(null), + map((x) => (x === this.item.phone ? '' : x)), + map((x) => (x !== null && x.length >= 1 ? x : null)), + debounceTime(150), + distinctUntilChanged(), + switchMap((x) => (x === null ? observableOf([]) : this.ser.autocomplete(x))), + ); + if (data) { + this.ser.get(data).subscribe((x) => this.showItem(x)); + } + } + + showItem(item: Customer) { + this.item = item; + this.form.patchValue({ + name: item.name, + phone: item.phone, + address: item.address, + printInBill: item.printInBill, + }); + this.form.setControl( + 'discounts', + this.fb.array( + item.discounts.map((x) => + this.fb.group({ + discount: '' + x.discount * 100, + }), + ), + ), + ); + this.form.markAsPristine(); + } + + save() { + const customer = this.getItem(); + const promise = this.form.pristine ? observableOf(customer) : this.ser.saveOrUpdate(customer); + promise.subscribe((x) => this.dialogRef.close(x)); + } + + displayFn(customer?: Customer | string): string { + if (!customer) { + return ''; + } + return typeof customer === 'string' ? customer : customer.phone; + } + + selected(event: MatAutocompleteSelectedEvent): void { + const customer: Customer = event.option.value; + this.showItem(customer); + } + + getItem(): Customer { + const formModel = this.form.value; + this.item.id = this.item.phone.trim() !== formModel.phone.trim() ? '' : this.item.id; + this.item.name = formModel.name; + this.item.phone = formModel.phone; + this.item.address = formModel.address; + this.item.printInBill = formModel.printInBill; + const array = this.form.get('discounts') as FormArray; + this.item.discounts.forEach((item, index) => { + item.discount = Math.max( + Math.min(round(array.controls[index].value.discount / 100, 5), 100), + 0, + ); + }); + return this.item; + } +} diff --git a/bookie/src/app/sales/sales.module.ts b/bookie/src/app/sales/sales.module.ts index 7c2568a..91128df 100644 --- a/bookie/src/app/sales/sales.module.ts +++ b/bookie/src/app/sales/sales.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FlexLayoutModule } from '@angular/flex-layout'; import { ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; @@ -25,6 +26,7 @@ import { BillTypeComponent } from './bill-type/bill-type.component'; import { BillService } from './bill.service'; import { BillsComponent } from './bills/bills.component'; import { CanDeactivateBillGuard } from './can-deactivate-bill.guard'; +import { ChooseCustomerComponent } from './choose-customer/choose-customer.component'; import { DiscountComponent } from './discount/discount.component'; import { SalesHomeComponent } from './home/sales-home.component'; import { MenuCategoriesComponent } from './menu-categories/menu-categories.component'; @@ -49,6 +51,7 @@ import { TablesDialogComponent } from './tables-dialog/tables-dialog.component'; MenuCategoriesComponent, ModifiersComponent, PaxComponent, + ChooseCustomerComponent, ProductsComponent, QuantityComponent, ReceivePaymentComponent, @@ -80,6 +83,7 @@ import { TablesDialogComponent } from './tables-dialog/tables-dialog.component'; SharedModule, SalesRoutingModule, MatSelectModule, + MatAutocompleteModule, ], }) export class SalesModule {}