Choose / Edit / Create customers during billing.

This commit is contained in:
Amritanshu Agrawal 2021-09-19 22:34:37 +05:30
parent c478290da0
commit 6379e5f4e3
14 changed files with 326 additions and 39 deletions

View File

@ -5,7 +5,7 @@ from typing import List
import barker.schemas.customer as schemas import barker.schemas.customer as schemas
from fastapi import APIRouter, Depends, HTTPException, Security, status 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.exc import SQLAlchemyError
from sqlalchemy.orm import Session 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: for discount in discounts:
cd = next((d for d in customer.discounts if d.sale_category_id == discount.id_), None) cd = next((d for d in customer.discounts if d.sale_category_id == discount.id_), None)
if cd is None: if cd is None:
@ -77,10 +77,10 @@ def add_discounts(customer: Customer, discounts: List[schemas.DiscountItem], db:
def delete_route( def delete_route(
id_: uuid.UUID, id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["customers"]), user: UserToken = Security(get_user, scopes=["customers"]),
): ) -> None:
with SessionFuture() as db: with SessionFuture() as db:
item: Customer = db.execute(select(Customer).where(Customer.id == id_)).scalar_one() db.execute(delete(CustomerDiscount).where(CustomerDiscount.customer_id == id_))
db.delete(item) db.execute(delete(Customer).where(Customer.id == id_))
db.commit() db.commit()
@ -88,7 +88,8 @@ def delete_route(
def show_blank( def show_blank(
user: UserToken = Security(get_user, scopes=["customers"]), user: UserToken = Security(get_user, scopes=["customers"]),
) -> schemas.CustomerBlank: ) -> schemas.CustomerBlank:
return blank_customer_info() with SessionFuture() as db:
return blank_customer_info(db)
@router.get("/list", response_model=List[schemas.Customer]) @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: def blank_customer_info(db: Session) -> schemas.CustomerBlank:
return schemas.CustomerBlank(name="", address="", phone="", printInBill=False, discounts=[]) 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()
],
)

View File

@ -1,3 +1,23 @@
.example-card { .example-card {
max-width: 400px; 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;
}

View File

@ -50,18 +50,16 @@
> >
<mat-checkbox formControlName="printInBill">Print in Bill?</mat-checkbox> <mat-checkbox formControlName="printInBill">Print in Bill?</mat-checkbox>
</div> </div>
<p></p>
<mat-divider></mat-divider> <div formArrayName="discounts" fxLayout="row wrap" class="discounts">
<div formArrayName="discounts">
<div <div
fxLayout="row"
*ngFor="let r of item.discounts; index as i" *ngFor="let r of item.discounts; index as i"
[formGroupName]="i" [formGroupName]="i"
fxLayout="row"
fxLayoutAlign="space-around start" fxLayoutAlign="space-around start"
fxLayout.lt-md="column" fxLayout.lt-md="column"
fxLayoutGap="20px" fxLayoutGap="20px"
fxLayoutGap.lt-md="0px" fxLayoutGap.lt-md="0px"
fxFlex="33%"
> >
<mat-form-field fxFlex> <mat-form-field fxFlex>
<mat-label>Discount on {{ r.name }}</mat-label> <mat-label>Discount on {{ r.name }}</mat-label>

View File

@ -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 { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -55,4 +55,13 @@ export class CustomerService {
.delete<Customer>(`${url}/${id}`, httpOptions) .delete<Customer>(`${url}/${id}`, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Customer>; .pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Customer>;
} }
autocomplete(query: string): Observable<Customer[]> {
const options = { params: new HttpParams().set('q', query) };
return this.http
.get<Customer[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<
Customer[]
>;
}
} }

View File

@ -1,12 +0,0 @@
export class Customer {
name: string;
phone: string;
address: string;
public constructor(init?: Partial<Customer>) {
this.name = '';
this.phone = '';
this.address = '';
Object.assign(this, init);
}
}

View File

@ -5,8 +5,9 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
import { Customer } from '../../core/customer';
import { ToasterService } from '../../core/toaster.service'; import { ToasterService } from '../../core/toaster.service';
import { Customer } from '../customer'; import { CustomerService } from '../../customers/customer.service';
import { GuestBook } from '../guest-book'; import { GuestBook } from '../guest-book';
import { GuestBookService } from '../guest-book.service'; import { GuestBookService } from '../guest-book.service';
@ -27,6 +28,7 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
private router: Router, private router: Router,
private toaster: ToasterService, private toaster: ToasterService,
private ser: GuestBookService, private ser: GuestBookService,
private customerService: CustomerService,
) { ) {
// Create form // Create form
this.form = this.fb.group({ this.form = this.fb.group({
@ -41,7 +43,7 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
map((x) => (x !== null && x.length >= 1 ? x : null)), map((x) => (x !== null && x.length >= 1 ? x : null)),
debounceTime(150), debounceTime(150),
distinctUntilChanged(), distinctUntilChanged(),
switchMap((x) => (x === null ? observableOf([]) : this.ser.autocomplete(x))), switchMap((x) => (x === null ? observableOf([]) : this.customerService.autocomplete(x))),
); );
} }

View File

@ -5,7 +5,6 @@ import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service'; import { ErrorLoggerService } from '../core/error-logger.service';
import { Customer } from './customer';
import { GuestBook } from './guest-book'; import { GuestBook } from './guest-book';
import { GuestBookList } from './guest-book-list'; import { GuestBookList } from './guest-book-list';
@ -59,13 +58,4 @@ export class GuestBookService {
.delete<GuestBook>(`${url}/${id}`, httpOptions) .delete<GuestBook>(`${url}/${id}`, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<GuestBook>; .pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<GuestBook>;
} }
autocomplete(query: string): Observable<Customer[]> {
const options = { params: new HttpParams().set('q', query) };
return this.http
.get<Customer[]>(`${customerUrl}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<
Customer[]
>;
}
} }

View File

@ -45,7 +45,9 @@
<mat-header-cell *matHeaderCellDef class="deep-purple-50 bold right-align" <mat-header-cell *matHeaderCellDef class="deep-purple-50 bold right-align"
><button>Table: {{ bs.bill.table.name }}</button> / ><button>Table: {{ bs.bill.table.name }}</button> /
<button (click)="choosePax()">{{ bs.bill.pax }} Pax</button> / <button (click)="choosePax()">{{ bs.bill.pax }} Pax</button> /
<button>{{ bs.bill.customer?.name || 'Customer' }}</button></mat-header-cell <button (click)="chooseCustomer()">
{{ bs.bill.customer?.name || 'Customer' }}
</button></mat-header-cell
> >
</ng-container> </ng-container>

View File

@ -6,11 +6,13 @@ import { map, switchMap } from 'rxjs/operators';
import { AuthService } from '../../auth/auth.service'; import { AuthService } from '../../auth/auth.service';
import { BillViewItem } from '../../core/bill-view-item'; import { BillViewItem } from '../../core/bill-view-item';
import { Customer } from '../../core/customer';
import { Table } from '../../core/table'; import { Table } from '../../core/table';
import { ToasterService } from '../../core/toaster.service'; import { ToasterService } from '../../core/toaster.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { TableService } from '../../tables/table.service'; import { TableService } from '../../tables/table.service';
import { BillService } from '../bill.service'; import { BillService } from '../bill.service';
import { ChooseCustomerComponent } from '../choose-customer/choose-customer.component';
import { PaxComponent } from '../pax/pax.component'; import { PaxComponent } from '../pax/pax.component';
import { QuantityComponent } from '../quantity/quantity.component'; import { QuantityComponent } from '../quantity/quantity.component';
import { TablesDialogComponent } from '../tables-dialog/tables-dialog.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 { isAllSelected(kotView: BillViewItem): boolean {
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot; const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
return kot.inventories.reduce( return kot.inventories.reduce(

View File

@ -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;
}

View File

@ -0,0 +1,90 @@
<h1 mat-dialog-title>Customer</h1>
<mat-dialog-content>
<form [formGroup]="form" fxLayout="column">
<div
fxLayout="row"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>Phone</mat-label>
<input
matInput
placeholder="Phone"
type="text"
formControlName="phone"
[matAutocomplete]="auto"
autocomplete="off"
cdkFocusInitial
/>
</mat-form-field>
<mat-autocomplete
#auto="matAutocomplete"
autoActiveFirstOption
[displayWith]="displayFn"
(optionSelected)="selected($event)"
>
<mat-option *ngFor="let customer of customers | async" [value]="customer"
>{{ customer.name }} - {{ customer.phone }}</mat-option
>
</mat-autocomplete>
</div>
<div
fxLayout="row"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>Name</mat-label>
<input matInput placeholder="Name" formControlName="name" />
</mat-form-field>
</div>
<div
fxLayout="row"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>Address</mat-label>
<textarea matInput placeholder="Address" formControlName="address"> </textarea>
</mat-form-field>
</div>
<div
fxLayout="row"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-checkbox formControlName="printInBill">Print in Bill?</mat-checkbox>
</div>
<p></p>
<div formArrayName="discounts" fxLayout="row wrap" class="discounts">
<div
*ngFor="let r of item.discounts; index as i"
[formGroupName]="i"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
fxFlex="33%"
>
<mat-form-field fxFlex>
<mat-label>Discount on {{ r.name }}</mat-label>
<input matInput placeholder="Discount" formControlName="discount" />
<span matSuffix>%</span>
</mat-form-field>
</div>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button [mat-dialog-close]="false">Cancel</button>
<button mat-raised-button color="primary" (click)="save()">Select</button>
</mat-dialog-actions>

View File

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

View File

@ -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<Customer[]>;
constructor(
public dialogRef: MatDialogRef<ChooseCustomerComponent>,
@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;
}
}

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout'; import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle'; 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 { BillService } from './bill.service';
import { BillsComponent } from './bills/bills.component'; import { BillsComponent } from './bills/bills.component';
import { CanDeactivateBillGuard } from './can-deactivate-bill.guard'; import { CanDeactivateBillGuard } from './can-deactivate-bill.guard';
import { ChooseCustomerComponent } from './choose-customer/choose-customer.component';
import { DiscountComponent } from './discount/discount.component'; import { DiscountComponent } from './discount/discount.component';
import { SalesHomeComponent } from './home/sales-home.component'; import { SalesHomeComponent } from './home/sales-home.component';
import { MenuCategoriesComponent } from './menu-categories/menu-categories.component'; import { MenuCategoriesComponent } from './menu-categories/menu-categories.component';
@ -49,6 +51,7 @@ import { TablesDialogComponent } from './tables-dialog/tables-dialog.component';
MenuCategoriesComponent, MenuCategoriesComponent,
ModifiersComponent, ModifiersComponent,
PaxComponent, PaxComponent,
ChooseCustomerComponent,
ProductsComponent, ProductsComponent,
QuantityComponent, QuantityComponent,
ReceivePaymentComponent, ReceivePaymentComponent,
@ -80,6 +83,7 @@ import { TablesDialogComponent } from './tables-dialog/tables-dialog.component';
SharedModule, SharedModule,
SalesRoutingModule, SalesRoutingModule,
MatSelectModule, MatSelectModule,
MatAutocompleteModule,
], ],
}) })
export class SalesModule {} export class SalesModule {}