toSignal via Gemini
This commit is contained in:
1 parent
59eb45da96
commit
993f83c786
350 files changed
+6231
-9801
No files matched your search
+17
-17
@@ -11,36 +11,36 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^21.1.1",
|
||||
"@angular/common": "^21.1.1",
|
||||
"@angular/compiler": "^21.1.1",
|
||||
"@angular/core": "^21.1.1",
|
||||
"@angular/forms": "^21.1.1",
|
||||
"@angular/material": "^21.1.1",
|
||||
"@angular/material-moment-adapter": "^21.1.1",
|
||||
"@angular/platform-browser": "^21.1.1",
|
||||
"@angular/platform-browser-dynamic": "^21.1.1",
|
||||
"@angular/router": "^21.1.1",
|
||||
"@angular/cdk": "^22.1.1",
|
||||
"@angular/common": "^22.1.1",
|
||||
"@angular/compiler": "^22.1.1",
|
||||
"@angular/core": "^22.1.1",
|
||||
"@angular/forms": "^22.1.1",
|
||||
"@angular/material": "^22.1.1",
|
||||
"@angular/material-moment-adapter": "^22.1.1",
|
||||
"@angular/platform-browser": "^22.1.1",
|
||||
"@angular/router": "^22.1.1",
|
||||
"mathjs": "^15.1.0",
|
||||
"moment": "^2.30.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^21.1.1",
|
||||
"@angular/cli": "^21.1.1",
|
||||
"@angular/compiler-cli": "^21.1.1",
|
||||
"angular-eslint": "^21.1.0",
|
||||
"eslint": "^9.28.0",
|
||||
"@angular/build": "^22.1.3",
|
||||
"@angular/cli": "^22.1.3",
|
||||
"@angular/compiler-cli": "^22.1.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"angular-eslint": "^22.1.0",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-perfectionist": "^5.4.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lint-staged": "^17.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vitest": "^4.0.8"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { provideRouter, withRouterConfig } from '@angular/router';
|
||||
import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
|
||||
|
||||
import { dateFormat } from './app.environment';
|
||||
import { routes } from './app.routes';
|
||||
@@ -55,6 +55,7 @@ export const appConfig: ApplicationConfig = {
|
||||
withRouterConfig({
|
||||
onSameUrlNavigation: 'reload',
|
||||
}),
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: dateFormat },
|
||||
|
||||
@@ -7,9 +7,12 @@ export const environment = {
|
||||
export const dateFormat = {
|
||||
parse: {
|
||||
dateInput: 'DD-MMM-YYYY',
|
||||
timeInput: 'hh:mm A',
|
||||
},
|
||||
display: {
|
||||
dateInput: 'DD-MMM-YYYY',
|
||||
timeInput: 'hh:mm A',
|
||||
timeOptionLabel: 'hh:mm A',
|
||||
monthYearLabel: 'MMM YYYY',
|
||||
dateA11yLabel: 'DD-MMM-YYYY',
|
||||
monthYearA11yLabel: 'MMM YYYY',
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<h2>Login</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput #nameElement formControlName="username" />
|
||||
<input matInput #nameElement [formField]="form.username" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput formControlName="password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
|
||||
<input matInput [formField]="form.password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
|
||||
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@if (unregisteredDevice) {
|
||||
@if (unregisteredDevice()) {
|
||||
<mat-divider></mat-divider>
|
||||
<h2>Sorry, device {{ deviceName }} is not enabled.</h2>
|
||||
<h2>Sorry, device {{ deviceName() }} is not enabled.</h2>
|
||||
}
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button (click)="login()" color="primary">Login</button>
|
||||
<button type="button" mat-raised-button (click)="login()" color="primary">Login</button>
|
||||
</div>
|
||||
@@ -1,5 +1,14 @@
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { form, FormField, required } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -11,13 +20,19 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CookieService } from '../../shared/cookie.service';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
export interface LoginFormData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css'],
|
||||
imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, ReactiveFormsModule],
|
||||
imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, FormField],
|
||||
})
|
||||
export class LoginComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
@@ -25,28 +40,26 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
private cs = inject(CookieService);
|
||||
private cd = inject(ChangeDetectorRef);
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
username: FormControl<string>;
|
||||
password: FormControl<string>;
|
||||
}>;
|
||||
formModel = signal<LoginFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
form = form(this.formModel, (schemaPath) => {
|
||||
required(schemaPath.username);
|
||||
required(schemaPath.password);
|
||||
});
|
||||
|
||||
hide: boolean;
|
||||
unregisteredDevice: boolean;
|
||||
deviceName: string;
|
||||
unregisteredDevice = signal<boolean | undefined>(undefined);
|
||||
deviceName = signal<string | undefined>(undefined);
|
||||
private returnUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.hide = true;
|
||||
this.unregisteredDevice = false;
|
||||
this.deviceName = '';
|
||||
this.unregisteredDevice.set(false);
|
||||
this.deviceName.set('');
|
||||
this.returnUrl = '';
|
||||
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
username: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
password: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -62,7 +75,7 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
login(): void {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
const username = formModel.username ?? '';
|
||||
const password = formModel.password ?? '';
|
||||
this.auth.login(username.trim(), password.trim()).subscribe({
|
||||
@@ -71,8 +84,8 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
},
|
||||
error: (error) => {
|
||||
if (error.status === 401 && error.error.detail === 'Device is not registered') {
|
||||
this.unregisteredDevice = true;
|
||||
this.deviceName = this.cs.getCookie('device');
|
||||
this.unregisteredDevice.set(true);
|
||||
this.deviceName.set(this.cs.getCookie('device'));
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
this.snackBar.open(error.error.detail, 'Error');
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { BeerSaleReportItem } from './beer-sale-report-item';
|
||||
|
||||
export class BeerSaleReportDataSource extends DataSource<BeerSaleReportItem> {
|
||||
constructor(public data: BeerSaleReportItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BeerSaleReportItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,70 +1,86 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Beer Sale Report</span>
|
||||
<button mat-icon-button (click)="exportCsv()">
|
||||
<button type="button" mat-icon-button (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
formControlName="startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #startDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Finish Date</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select formControlName="saleCategory" (selectionChange)="filterOn($event.value)">
|
||||
@for (sc of saleCategories; track sc) {
|
||||
<mat-option [value]="sc.id">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
@if (!infoResource.value() || !saleCategoriesResource.value()) {
|
||||
<p>Loading...</p>
|
||||
} @else {
|
||||
<form class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #startDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Finish Date</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select [formField]="form.saleCategory" (selectionChange)="filterOn($event.value)">
|
||||
@for (sc of saleCategories(); track sc) {
|
||||
<mat-option [value]="sc.id">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@for (error of form.saleCategory().errors(); track error) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-checkbox class="flex-auto" formControlName="regular">Regular</mat-checkbox>
|
||||
<mat-checkbox class="flex-auto" formControlName="happy">Happy Hour</mat-checkbox>
|
||||
<mat-checkbox class="flex-auto" formControlName="staff">Staff Consumption</mat-checkbox>
|
||||
<mat-checkbox class="flex-auto" formControlName="nc">No Charge</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef>Time</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.date }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
@for (col of info.headers; track col) {
|
||||
<ng-container matColumnDef="{{ col }}">
|
||||
<mat-header-cell *matHeaderCellDef class="right">{{ col }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row[col] | number: '1.2-2' }}</mat-cell>
|
||||
</mat-form-field>
|
||||
<button
|
||||
type="button"
|
||||
mat-raised-button
|
||||
class="flex-auto basis-1-5"
|
||||
color="primary"
|
||||
[disabled]="form().invalid()"
|
||||
(click)="show()"
|
||||
>
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-checkbox class="flex-auto" [formField]="form.regular">Regular</mat-checkbox>
|
||||
<mat-checkbox class="flex-auto" [formField]="form.happy">Happy Hour</mat-checkbox>
|
||||
<mat-checkbox class="flex-auto" [formField]="form.staff">Staff Consumption</mat-checkbox>
|
||||
<mat-checkbox class="flex-auto" [formField]="form.nc">No Charge</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef>Time</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.date }}</mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
<!-- Name Column -->
|
||||
@for (col of info()?.headers ?? []; track col) {
|
||||
<ng-container matColumnDef="{{ col }}">
|
||||
<mat-header-cell *matHeaderCellDef class="right">{{ col }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row[col] | number: '1.2-2' }}</mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns()"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns()"></mat-row>
|
||||
</mat-table>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField, required } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
@@ -13,10 +13,21 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
|
||||
import { SaleCategory } from '../core/sale-category';
|
||||
import { SaleCategoryService } from '../sale-category/sale-category.service';
|
||||
import { ToCsvService } from '../shared/to-csv.service';
|
||||
import { BeerSaleExportHeaderInterface } from './beer-sale-export-header-interface';
|
||||
import { BeerSaleReport } from './beer-sale-report';
|
||||
import { BeerSaleReportDataSource } from './beer-sale-report-datasource';
|
||||
import { BeerSaleReportService } from './beer-sale-report.service';
|
||||
|
||||
export interface BeerSaleReportFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
saleCategory: string;
|
||||
regular: boolean;
|
||||
happy: boolean;
|
||||
staff: boolean;
|
||||
nc: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-beer-sale-report',
|
||||
@@ -32,61 +43,72 @@ import { BeerSaleReportDataSource } from './beer-sale-report-datasource';
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class BeerSaleReportComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class BeerSaleReportComponent {
|
||||
private router = inject(Router);
|
||||
private route = inject(ActivatedRoute);
|
||||
private toCsv = inject(ToCsvService);
|
||||
private ser = inject(BeerSaleReportService);
|
||||
private saleCategoryService = inject(SaleCategoryService);
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
saleCategory = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
regular = input(true, { transform: (v: string | null | undefined) => v !== 'false' });
|
||||
happy = input(true, { transform: (v: string | null | undefined) => v !== 'false' });
|
||||
staff = input(true, { transform: (v: string | null | undefined) => v !== 'false' });
|
||||
nc = input(true, { transform: (v: string | null | undefined) => v !== 'false' });
|
||||
|
||||
saleCategories: SaleCategory[] = [];
|
||||
info: BeerSaleReport = new BeerSaleReport();
|
||||
dataSource: BeerSaleReportDataSource = new BeerSaleReportDataSource(this.info.data);
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
saleCategory: FormControl<string>;
|
||||
regular: FormControl<boolean>;
|
||||
happy: FormControl<boolean>;
|
||||
staff: FormControl<boolean>;
|
||||
nc: FormControl<boolean>;
|
||||
}>;
|
||||
saleCategoriesResource = this.saleCategoryService.list();
|
||||
infoResource = this.ser.get(
|
||||
this.startDate,
|
||||
this.finishDate,
|
||||
this.saleCategory,
|
||||
this.regular,
|
||||
this.happy,
|
||||
this.staff,
|
||||
this.nc,
|
||||
);
|
||||
|
||||
saleCategories = computed(() => this.saleCategoriesResource.value() ?? []);
|
||||
info = computed(() => this.infoResource.value());
|
||||
dataSource = computed(() => this.info()?.data ?? []);
|
||||
formModel = linkedSignal({
|
||||
source: () => this.info(),
|
||||
computation: (info): BeerSaleReportFormData => {
|
||||
if (info) {
|
||||
return {
|
||||
startDate: moment(info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
saleCategory: info.saleCategory?.id ?? '',
|
||||
regular: info.regular,
|
||||
happy: info.happy,
|
||||
staff: info.staff,
|
||||
nc: info.nc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
startDate: new Date(),
|
||||
finishDate: new Date(),
|
||||
saleCategory: '',
|
||||
regular: true,
|
||||
happy: true,
|
||||
staff: true,
|
||||
nc: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel, (schemaPath) => {
|
||||
required(schemaPath.saleCategory, { message: 'Sale Category is required' });
|
||||
});
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns: string[] = ['date'];
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
saleCategory: new FormControl('', { nonNullable: true }),
|
||||
regular: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
happy: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
staff: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
nc: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: BeerSaleReport; saleCategories: SaleCategory[] };
|
||||
this.info = data.info;
|
||||
this.saleCategories = data.saleCategories;
|
||||
this.displayedColumns = ['date'].concat(this.info.headers);
|
||||
this.form.setValue({
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
saleCategory: this.info.saleCategory?.id ?? '',
|
||||
regular: this.info.regular,
|
||||
happy: this.info.happy,
|
||||
staff: this.info.staff,
|
||||
nc: this.info.nc,
|
||||
});
|
||||
this.dataSource = new BeerSaleReportDataSource(this.info.data);
|
||||
});
|
||||
}
|
||||
displayedColumns = computed(() => {
|
||||
const headers = this.info()?.headers ?? [];
|
||||
return ['date'].concat(headers);
|
||||
});
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
@@ -104,17 +126,21 @@ export class BeerSaleReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
filterOn(id: string) {
|
||||
const sc = this.saleCategories.find((x) => x.id === id);
|
||||
this.info.saleCategory = sc ? sc : new SaleCategory({ id });
|
||||
const sc = this.saleCategories().find((x: SaleCategory) => x.id === id);
|
||||
const currentInfo = this.info();
|
||||
if (currentInfo) {
|
||||
currentInfo.saleCategory = sc ? sc : new SaleCategory({ id });
|
||||
}
|
||||
}
|
||||
|
||||
getInfo(): BeerSaleReport {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
const currentInfo = this.info();
|
||||
|
||||
return new BeerSaleReport({
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
|
||||
saleCategory: this.info.saleCategory,
|
||||
saleCategory: currentInfo?.saleCategory,
|
||||
regular: formModel.regular,
|
||||
happy: formModel.happy,
|
||||
staff: formModel.staff,
|
||||
@@ -123,12 +149,18 @@ export class BeerSaleReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
exportCsv() {
|
||||
const currentInfo = this.info();
|
||||
if (!currentInfo) return;
|
||||
|
||||
const init: BeerSaleExportHeaderInterface = { Date: 'date' };
|
||||
const headers = this.info.headers.reduce((a, c) => {
|
||||
a[c] = c;
|
||||
return a;
|
||||
}, init);
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.info.data)], {
|
||||
const headers = currentInfo.headers.reduce(
|
||||
(a: Record<string, string>, c: string) => {
|
||||
a[c] = c;
|
||||
return a;
|
||||
},
|
||||
init as Record<string, string>,
|
||||
);
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, currentInfo.data)], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BeerSaleReportResolver } from './beer-sale-report-resolver.service';
|
||||
|
||||
describe('BeerSaleReportResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BeerSaleReportResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BeerSaleReportResolver], (service: BeerSaleReportResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { BeerSaleReport } from './beer-sale-report';
|
||||
import { BeerSaleReportService } from './beer-sale-report.service';
|
||||
|
||||
export const beerSaleReportResolver: ResolveFn<BeerSaleReport> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
const saleCategory = route.queryParamMap.get('saleCategory') ?? null;
|
||||
const regular = route.queryParamMap.get('regular') !== 'false';
|
||||
const happy = route.queryParamMap.get('happy') !== 'false';
|
||||
const staff = route.queryParamMap.get('staff') !== 'false';
|
||||
const nc = route.queryParamMap.get('nc') !== 'false';
|
||||
return inject(BeerSaleReportService).get(startDate, finishDate, saleCategory, regular, happy, staff, nc);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { saleCategoryListResolver } from '../sale-category/sale-category-list.resolver';
|
||||
import { BeerSaleReportComponent } from './beer-sale-report.component';
|
||||
import { beerSaleReportResolver } from './beer-sale-report.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Beer Sale Report',
|
||||
},
|
||||
resolve: {
|
||||
info: beerSaleReportResolver,
|
||||
saleCategories: saleCategoryListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,46 +1,42 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { BeerSaleReport } from './beer-sale-report';
|
||||
|
||||
const url = '/api/beer-sale-report';
|
||||
const serviceName = 'BeerSaleReportService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BeerSaleReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(
|
||||
startDate: string | null,
|
||||
finishDate: string | null,
|
||||
saleCategory: string | null,
|
||||
regular: boolean,
|
||||
happy: boolean,
|
||||
staff: boolean,
|
||||
nc: boolean,
|
||||
): Observable<BeerSaleReport> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
if (saleCategory != null) {
|
||||
options.params = options.params.set('sc', saleCategory);
|
||||
}
|
||||
options.params = options.params.set('r', regular);
|
||||
options.params = options.params.set('h', happy);
|
||||
options.params = options.params.set('st', staff);
|
||||
options.params = options.params.set('n', nc);
|
||||
return this.http
|
||||
.get<BeerSaleReport>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable<BeerSaleReport>;
|
||||
startDate: Signal<string | null>,
|
||||
finishDate: Signal<string | null>,
|
||||
saleCategory: Signal<string | null>,
|
||||
regular: Signal<boolean>,
|
||||
happy: Signal<boolean>,
|
||||
staff: Signal<boolean>,
|
||||
nc: Signal<boolean>,
|
||||
): HttpResourceRef<BeerSaleReport | undefined> {
|
||||
return httpResource<BeerSaleReport>(() => {
|
||||
let params = new HttpParams();
|
||||
const s = startDate();
|
||||
const f = finishDate();
|
||||
const sc = saleCategory();
|
||||
if (s !== null) {
|
||||
params = params.set('s', s);
|
||||
}
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
if (sc !== null) {
|
||||
params = params.set('sc', sc);
|
||||
}
|
||||
params = params.set('r', regular());
|
||||
params = params.set('h', happy());
|
||||
params = params.set('st', staff());
|
||||
params = params.set('n', nc());
|
||||
return { url, params };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { BillSettlementReportItem } from './bill-settlement-report-item';
|
||||
|
||||
export class BillSettlementReportDataSource extends DataSource<BillSettlementReportItem> {
|
||||
constructor(public data: BillSettlementReportItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BillSettlementReportItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Bill Settlement Report</span>
|
||||
<button mat-icon-button (click)="exportCsv()">
|
||||
<button type="button" mat-icon-button (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
@@ -13,7 +13,7 @@
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
formControlName="startDate"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
@@ -25,16 +25,16 @@
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
<button type="button" mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef>Time</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -13,7 +13,12 @@ import moment from 'moment';
|
||||
import { LocalTimePipe } from '../shared/local-time.pipe';
|
||||
import { ToCsvService } from '../shared/to-csv.service';
|
||||
import { BillSettlementReport } from './bill-settlement-report';
|
||||
import { BillSettlementReportDataSource } from './bill-settlement-report-datasource';
|
||||
import { BillSettlementReportService } from './bill-settlement-report.service';
|
||||
|
||||
export interface BillSettlementReportFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-bill-settlement-report',
|
||||
@@ -23,51 +28,47 @@ import { BillSettlementReportDataSource } from './bill-settlement-report-datasou
|
||||
DecimalPipe,
|
||||
LocalTimePipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class BillSettlementReportComponent implements OnInit {
|
||||
export class BillSettlementReportComponent {
|
||||
private billSettlementReportService = inject(BillSettlementReportService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private toCsv = inject(ToCsvService);
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
info: BillSettlementReport = new BillSettlementReport();
|
||||
dataSource: BillSettlementReportDataSource = new BillSettlementReportDataSource(this.info.amounts);
|
||||
infoResource = this.billSettlementReportService.get(this.startDate, this.finishDate);
|
||||
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
}>;
|
||||
info = computed(() => this.infoResource.value() ?? new BillSettlementReport());
|
||||
dataSource = computed(() => this.info().amounts);
|
||||
formModel = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (infoVal): BillSettlementReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: moment(this.info().startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info().finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
startDate: new Date(),
|
||||
finishDate: new Date(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['date', 'billId', 'amount', 'settlement'];
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: BillSettlementReport };
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
});
|
||||
this.dataSource = new BillSettlementReportDataSource(this.info.amounts);
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
this.router.navigate(['bill-settlement-report'], {
|
||||
@@ -79,7 +80,7 @@ export class BillSettlementReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
getInfo(): BillSettlementReport {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new BillSettlementReport({
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
@@ -94,7 +95,7 @@ export class BillSettlementReportComponent implements OnInit {
|
||||
Amount: 'amount',
|
||||
Settlement: 'settlement',
|
||||
};
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource())], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BillSettlementReportResolver } from './bill-settlement-report-resolver.service';
|
||||
|
||||
describe('BillSettlementReportResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BillSettlementReportResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BillSettlementReportResolver], (service: BillSettlementReportResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { BillSettlementReport } from './bill-settlement-report';
|
||||
import { BillSettlementReportService } from './bill-settlement-report.service';
|
||||
|
||||
export const billSettlementReportResolver: ResolveFn<BillSettlementReport> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(BillSettlementReportService).get(startDate, finishDate);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { BillSettlementReportComponent } from './bill-settlement-report.component';
|
||||
import { billSettlementReportResolver } from './bill-settlement-report.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Bill Settlement Report',
|
||||
},
|
||||
resolve: {
|
||||
info: billSettlementReportResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,31 +1,29 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { BillSettlementReport } from './bill-settlement-report';
|
||||
|
||||
const url = '/api/bill-settlement-report';
|
||||
const serviceName = 'BillSettlementReportService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BillSettlementReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(startDate: string | null, finishDate: string | null): Observable<BillSettlementReport> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
return this.http
|
||||
.get<BillSettlementReport>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable<BillSettlementReport>;
|
||||
get(
|
||||
startDate: Signal<string | null>,
|
||||
finishDate: Signal<string | null>,
|
||||
): HttpResourceRef<BillSettlementReport | undefined> {
|
||||
return httpResource<BillSettlementReport>(() => {
|
||||
let params = new HttpParams();
|
||||
const s = startDate();
|
||||
const f = finishDate();
|
||||
if (s !== null) {
|
||||
params = params.set('s', s);
|
||||
}
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return { url, params };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { BundleItem } from '../bundle';
|
||||
|
||||
export class BundleDetailDatasource extends DataSource<BundleItem> {
|
||||
constructor(private data: Observable<BundleItem[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BundleItem[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<h1 mat-dialog-title>Edit Bundle Item</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="form">
|
||||
<form>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Item</mat-label>
|
||||
<input matInput type="text" formControlName="product" [matAutocomplete]="autoP" />
|
||||
<input matInput type="text" [formField]="form.product" [matAutocomplete]="autoP" />
|
||||
<mat-autocomplete #autoP="matAutocomplete" [displayWith]="displayFn" (optionSelected)="productSelected($event)">
|
||||
@for (p of products$ | async; track p) {
|
||||
@for (p of products(); track p) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@@ -15,19 +15,19 @@
|
||||
<div class="flex flex-row flex-wrap gap-3">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Quantity</mat-label>
|
||||
<input matInput type="number" formControlName="quantity" />
|
||||
<input matInput type="number" [formField]="form.quantity" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" formControlName="salePrice" />
|
||||
<input matInput type="number" [formField]="form.salePrice" />
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="printInBill" class="flex-auto">Print in Bill?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.printInBill" class="flex-auto">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions>
|
||||
<button mat-raised-button color="warn" [mat-dialog-close]="false">Cancel</button>
|
||||
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
</div>
|
||||
@@ -1,18 +1,24 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, OnInit, inject, signal, computed, effect, debounced } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
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';
|
||||
import { debounceTime, distinctUntilChanged, Observable, of as observableOf, switchMap } from 'rxjs';
|
||||
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { ProductService } from '../../product/product.service';
|
||||
import { BundleItem } from '../bundle';
|
||||
|
||||
export interface BundleDetailDialogFormData {
|
||||
product: ProductQuery | string;
|
||||
quantity: number;
|
||||
salePrice: number;
|
||||
printInBill: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-detail-dialog',
|
||||
templateUrl: './bundle-detail-dialog.component.html',
|
||||
@@ -26,7 +32,7 @@ import { BundleItem } from '../bundle';
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class BundleDetailDialogComponent implements OnInit {
|
||||
@@ -35,49 +41,48 @@ export class BundleDetailDialogComponent implements OnInit {
|
||||
|
||||
data = inject<{ item: BundleItem }>(MAT_DIALOG_DATA);
|
||||
|
||||
selectedProduct: ProductQuery | null = null;
|
||||
products$: Observable<ProductQuery[]>;
|
||||
selectedProduct = signal<ProductQuery | null>(null);
|
||||
|
||||
form: FormGroup<{
|
||||
product: FormControl<ProductQuery | string>;
|
||||
quantity: FormControl<number>;
|
||||
salePrice: FormControl<number>;
|
||||
printInBill: FormControl<boolean>;
|
||||
}>;
|
||||
formModel = signal<BundleDetailDialogFormData>({
|
||||
product: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
productSignal = computed(() => {
|
||||
const p = this.formModel().product;
|
||||
return typeof p === 'string' ? p.trim() : '';
|
||||
});
|
||||
|
||||
debouncedProduct = debounced(this.productSignal, 150);
|
||||
|
||||
productsResource = this.productSer.query(computed(() => this.debouncedProduct.value()));
|
||||
products = computed(() => {
|
||||
if (!this.debouncedProduct.value()) return [];
|
||||
return this.productsResource.value() ?? [];
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
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(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => {
|
||||
if (typeof x !== 'string') return observableOf([]);
|
||||
return x.trim() === '' ? observableOf([]) : this.productSer.query(x);
|
||||
}),
|
||||
);
|
||||
|
||||
// typing after selecting invalidates prior selection
|
||||
this.form.controls.product.valueChanges.subscribe((x) => {
|
||||
if (typeof x === 'string') this.selectedProduct = null;
|
||||
effect(() => {
|
||||
const x = this.formModel().product;
|
||||
if (typeof x === 'string') this.selectedProduct.set(null);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// preload display text using existing name
|
||||
this.form.setValue({
|
||||
this.formModel.set({
|
||||
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;
|
||||
this.selectedProduct.set(null);
|
||||
}
|
||||
|
||||
displayFn(product?: ProductQuery | string): string {
|
||||
@@ -86,11 +91,11 @@ export class BundleDetailDialogComponent implements OnInit {
|
||||
|
||||
productSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const p = event.option.value as ProductQuery;
|
||||
this.selectedProduct = p;
|
||||
this.selectedProduct.set(p);
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
const v = this.form.value;
|
||||
const v = this.formModel();
|
||||
|
||||
const quantity = Number(v.quantity ?? 0);
|
||||
if (Number.isNaN(quantity) || quantity <= 0) return;
|
||||
@@ -99,9 +104,9 @@ export class BundleDetailDialogComponent implements OnInit {
|
||||
if (Number.isNaN(salePrice) || salePrice < 0) return;
|
||||
|
||||
// If a new product was selected, update itemId + name
|
||||
if (this.selectedProduct?.id) {
|
||||
this.data.item.itemId = this.selectedProduct.id;
|
||||
this.data.item.name = this.selectedProduct.name ?? '';
|
||||
if (this.selectedProduct()?.id) {
|
||||
this.data.item.itemId = this.selectedProduct()?.id ?? '';
|
||||
this.data.item.name = this.selectedProduct()?.name ?? '';
|
||||
} else {
|
||||
// If no selection, don't allow changing SKU silently
|
||||
// but still allow quantity/price edit
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<h2>Bundle</h2>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="basis-3-4">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #name formControlName="name" />
|
||||
<input matInput #name [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="basis-1-4">
|
||||
<mat-label>Units</mat-label>
|
||||
<input matInput formControlName="units" />
|
||||
<input matInput [formField]="form.units" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" formControlName="salePrice" readonly="" />
|
||||
<input matInput type="number" [value]="formModel().salePrice" readonly />
|
||||
<mat-hint>Computed from bundle items</mat-hint>
|
||||
</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-select [formField]="form.menuCategory">
|
||||
@for (mc of menuCategories(); track mc) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select formControlName="saleCategory">
|
||||
@for (sc of saleCategories; track sc) {
|
||||
<mat-select [formField]="form.saleCategory">
|
||||
@for (sc of saleCategories(); track sc) {
|
||||
<mat-option [value]="sc.id">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
@@ -49,24 +49,24 @@
|
||||
>
|
||||
</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>
|
||||
<mat-checkbox [formField]="form.hasHappyHour" class="flex-auto">Has Happy Hour?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.isNotAvailable" class="flex-auto">Not Available?</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<h2>Items</h2>
|
||||
|
||||
<div formGroupName="addRow" class="nutrition-grid">
|
||||
<div class="nutrition-grid">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Product</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
formControlName="itemId"
|
||||
[formField]="form.addRow.itemId"
|
||||
[matAutocomplete]="autoItem"
|
||||
[value]="displayFn(form.controls.addRow.controls.itemId.value)"
|
||||
[value]="displayFn(formModel().addRow.itemId)"
|
||||
/>
|
||||
<mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn" (optionSelected)="bundleSelected($event)">
|
||||
@for (p of itemProducts | async; track p) {
|
||||
@for (p of itemProducts(); track p) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@@ -74,21 +74,21 @@
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Quantity</mat-label>
|
||||
<input matInput type="number" formControlName="quantity" />
|
||||
<input matInput type="number" [formField]="form.addRow.quantity" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" formControlName="salePrice" />
|
||||
<input matInput type="number" [formField]="form.addRow.salePrice" />
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="printInBill" class="basis-1-4">Print in Bill?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.addRow.printInBill" class="basis-1-4">Print in Bill?</mat-checkbox>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container wrapped">
|
||||
<mat-table [dataSource]="dataSource" aria-label="Bundle Items" class="flex-auto">
|
||||
<mat-table [dataSource]="dataSource()" aria-label="Bundle Items" class="flex-auto">
|
||||
<!-- Name Column (name is "name (units)") -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Item</mat-header-cell>
|
||||
@@ -127,10 +127,10 @@
|
||||
<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 type="button" mat-icon-button tabindex="-1" (click)="editRow(row)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
|
||||
<button type="button" mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
@@ -142,8 +142,8 @@
|
||||
</div>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
</div>
|
||||
@@ -1,6 +1,18 @@
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
inject,
|
||||
computed,
|
||||
signal,
|
||||
effect,
|
||||
debounced,
|
||||
linkedSignal,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
@@ -13,24 +25,39 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { of as observableOf, BehaviorSubject, debounceTime, distinctUntilChanged, Observable, switchMap } from 'rxjs';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { SaleCategory } from '../../core/sale-category';
|
||||
import { MenuCategoryService } from '../../menu-category/menu-category.service';
|
||||
import { ProductService } from '../../product/product.service';
|
||||
import { SaleCategoryService } from '../../sale-category/sale-category.service';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { Bundle, BundleItem } from '../bundle';
|
||||
import { BundleService } from '../bundle.service';
|
||||
import { BundleDetailDatasource } from './bundle-detail-datasource';
|
||||
import { BundleDetailDialogComponent } from './bundle-detail-dialog.component';
|
||||
|
||||
export interface BundleDetailFormData {
|
||||
name: string;
|
||||
units: string;
|
||||
salePrice: number;
|
||||
menuCategory: string;
|
||||
saleCategory: string;
|
||||
hasHappyHour: boolean;
|
||||
isNotAvailable: boolean;
|
||||
addRow: {
|
||||
itemId: ProductQuery | string;
|
||||
quantity: number;
|
||||
salePrice: number;
|
||||
printInBill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-detail',
|
||||
templateUrl: './bundle-detail.component.html',
|
||||
styleUrls: ['./bundle-detail.component.css'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
@@ -41,95 +68,113 @@ import { BundleDetailDialogComponent } from './bundle-detail-dialog.component';
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
export class BundleDetailComponent implements AfterViewInit {
|
||||
@ViewChild('name', { static: true }) nameElement?: ElementRef;
|
||||
private menuCategoryService = inject(MenuCategoryService);
|
||||
private saleCategoryService = inject(SaleCategoryService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(BundleService);
|
||||
private productSer = inject(ProductService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('name', { static: true }) nameElement?: ElementRef;
|
||||
saleCategoriesResource = this.saleCategoryService.list();
|
||||
|
||||
form: FormGroup<{
|
||||
// Bundle header fields
|
||||
name: FormControl<string>;
|
||||
units: FormControl<string>;
|
||||
salePrice: FormControl<number>;
|
||||
menuCategory: FormControl<string>;
|
||||
saleCategory: FormControl<string>;
|
||||
hasHappyHour: FormControl<boolean>;
|
||||
isNotAvailable: FormControl<boolean>;
|
||||
menuCategoriesResource = this.menuCategoryService.list(undefined);
|
||||
|
||||
// Item add row
|
||||
addRow: FormGroup<{
|
||||
itemId: FormControl<ProductQuery | string>;
|
||||
quantity: FormControl<number>;
|
||||
salePrice: FormControl<number>;
|
||||
printInBill: FormControl<boolean>;
|
||||
}>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
menuCategories: MenuCategory[] = [];
|
||||
saleCategories: SaleCategory[] = [];
|
||||
public items$ = new BehaviorSubject<BundleItem[]>([]);
|
||||
dataSource: BundleDetailDatasource = new BundleDetailDatasource(this.items$);
|
||||
formModel = linkedSignal({
|
||||
source: () => this.menuCategoriesResource.value(),
|
||||
computation: (menuCategoriesVal): BundleDetailFormData => {
|
||||
if (menuCategoriesVal) {
|
||||
return {
|
||||
name: this.item.name ?? '',
|
||||
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,
|
||||
|
||||
addRow: {
|
||||
itemId: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: '',
|
||||
units: '',
|
||||
salePrice: 0,
|
||||
menuCategory: '',
|
||||
saleCategory: '',
|
||||
hasHappyHour: false,
|
||||
isNotAvailable: false,
|
||||
addRow: {
|
||||
itemId: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
menuCategories = signal<MenuCategory[]>([]);
|
||||
saleCategories = signal<SaleCategory[]>([]);
|
||||
items = signal<BundleItem[]>([]);
|
||||
dataSource = computed(() => this.items());
|
||||
|
||||
item: Bundle = new Bundle();
|
||||
|
||||
itemProduct: ProductQuery | null = null;
|
||||
itemProducts: Observable<ProductQuery[]>;
|
||||
itemProduct = signal<ProductQuery | null>(null);
|
||||
|
||||
itemIdSignal = computed(() => {
|
||||
const x = this.formModel().addRow?.itemId;
|
||||
return typeof x === 'string' ? x.trim() : '';
|
||||
});
|
||||
|
||||
debouncedItemId = debounced(this.itemIdSignal, 150);
|
||||
|
||||
itemProductsResource = this.productSer.query(computed(() => this.debouncedItemId.value()));
|
||||
itemProducts = computed(() => {
|
||||
if (!this.debouncedItemId.value()) return [];
|
||||
return this.itemProductsResource.value() ?? [];
|
||||
});
|
||||
|
||||
displayedColumns = ['name', 'quantity', 'salePrice', 'printInBill', 'action'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
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 }),
|
||||
|
||||
addRow: new FormGroup({
|
||||
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(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => {
|
||||
// if user types, x is string; if user selects, x is Product
|
||||
if (typeof x !== 'string') {
|
||||
return observableOf([]);
|
||||
}
|
||||
return x.trim() === '' ? observableOf([]) : this.productSer.query(x);
|
||||
}),
|
||||
);
|
||||
this.form.controls.addRow.controls.itemId.valueChanges.subscribe((x) => {
|
||||
effect(() => {
|
||||
const x = this.formModel().addRow?.itemId;
|
||||
if (typeof x === 'string') {
|
||||
this.itemProduct = null;
|
||||
this.itemProduct.set(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
effect(() => {
|
||||
const menuCategoriesVal = this.menuCategoriesResource.value();
|
||||
if (menuCategoriesVal) this.menuCategories.set(menuCategoriesVal);
|
||||
const saleCategoriesVal = this.saleCategoriesResource.value();
|
||||
if (saleCategoriesVal) this.saleCategories.set(saleCategoriesVal);
|
||||
const itemVal = this.itemResource.value();
|
||||
if (itemVal) {
|
||||
this.item = itemVal;
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as {
|
||||
item: Bundle;
|
||||
menuCategories: MenuCategory[];
|
||||
saleCategories: SaleCategory[];
|
||||
};
|
||||
this.menuCategories = data.menuCategories;
|
||||
this.saleCategories = data.saleCategories;
|
||||
this.showItem(data.item);
|
||||
this.itemProduct.set(null);
|
||||
|
||||
this.items.set(this.item.items ?? []);
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,8 +184,14 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
|
||||
bundleSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const product = event.option.value as ProductQuery;
|
||||
this.itemProduct = product;
|
||||
this.form.controls.addRow.controls.salePrice.setValue(product.price ?? 0);
|
||||
this.itemProduct.set(product);
|
||||
this.formModel.update((f) => ({
|
||||
...f,
|
||||
addRow: {
|
||||
...f.addRow,
|
||||
salePrice: product.price ?? 0,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private recalcBundleSalePrice(): void {
|
||||
@@ -154,34 +205,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
// keep model + form in sync
|
||||
this.item.salePrice = Number(total.toFixed(2));
|
||||
|
||||
// if the control is disabled, use patchValue
|
||||
this.form.controls.salePrice.patchValue(this.item.salePrice);
|
||||
}
|
||||
|
||||
showItem(item: Bundle) {
|
||||
this.item = item;
|
||||
|
||||
this.form.setValue({
|
||||
name: this.item.name ?? '',
|
||||
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,
|
||||
|
||||
addRow: {
|
||||
itemId: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
},
|
||||
});
|
||||
this.itemProduct = null;
|
||||
this.form.controls.salePrice.disable({ emitEvent: false });
|
||||
|
||||
this.items$.next(this.item.items ?? []);
|
||||
this.recalcBundleSalePrice();
|
||||
this.formModel.update((f) => ({ ...f, salePrice: this.item.salePrice }));
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@@ -193,17 +217,25 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
resetAddRow() {
|
||||
this.form.controls.addRow.reset();
|
||||
this.itemProduct = null;
|
||||
this.formModel.update((f) => ({
|
||||
...f,
|
||||
addRow: {
|
||||
itemId: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
},
|
||||
}));
|
||||
this.itemProduct.set(null);
|
||||
}
|
||||
|
||||
addRow() {
|
||||
const v = this.form.value.addRow;
|
||||
const v = this.formModel().addRow;
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.itemProduct?.id) {
|
||||
if (!this.itemProduct()?.id) {
|
||||
this.snackBar.open('Please select a product', 'Error');
|
||||
return;
|
||||
}
|
||||
@@ -223,15 +255,15 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
// name is expected from backend as "name (units)" on GET;
|
||||
// for new rows we keep it blank (or you can set placeholder)
|
||||
const bi = new BundleItem({
|
||||
itemId: this.itemProduct?.id,
|
||||
name: this.itemProduct?.name ?? '',
|
||||
itemId: this.itemProduct()?.id,
|
||||
name: this.itemProduct()?.name ?? '',
|
||||
quantity,
|
||||
salePrice,
|
||||
printInBill: v.printInBill || false,
|
||||
});
|
||||
|
||||
this.item.items.push(bi);
|
||||
this.items$.next(this.item.items);
|
||||
this.items.set(this.item.items);
|
||||
this.resetAddRow();
|
||||
|
||||
this.recalcBundleSalePrice();
|
||||
@@ -250,7 +282,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
return;
|
||||
}
|
||||
Object.assign(row, result as BundleItem);
|
||||
this.items$.next(this.item.items);
|
||||
this.items.set(this.item.items);
|
||||
this.resetAddRow();
|
||||
this.recalcBundleSalePrice();
|
||||
});
|
||||
@@ -258,7 +290,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
|
||||
deleteRow(row: BundleItem) {
|
||||
this.item.items.splice(this.item.items.indexOf(row), 1);
|
||||
this.items$.next(this.item.items);
|
||||
this.items.set(this.item.items);
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
|
||||
@@ -272,8 +304,8 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/bundles');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -284,8 +316,8 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/bundles');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -304,7 +336,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Bundle {
|
||||
const v = this.form.value;
|
||||
const v = this.formModel();
|
||||
|
||||
this.item.name = v.name ?? '';
|
||||
this.item.units = v.units ?? '';
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleListResolver } from './bundle-list-resolver.service';
|
||||
|
||||
describe('BundleListResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleListResolver], (service: BundleListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Bundle } from './bundle';
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
export const bundleListResolver: ResolveFn<Bundle[]> = () => {
|
||||
return inject(BundleService).list();
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { Bundle } from '../bundle';
|
||||
|
||||
export class BundleListDataSource extends DataSource<Bundle> {
|
||||
public data: Bundle[];
|
||||
public filteredData: Bundle[];
|
||||
public search: string;
|
||||
public menuCategory: string;
|
||||
|
||||
constructor(
|
||||
private readonly searchFilter: Observable<string>,
|
||||
private readonly menuCategoryFilter: Observable<string>,
|
||||
private readonly dataObs: Observable<Bundle[]>,
|
||||
) {
|
||||
super();
|
||||
this.data = [];
|
||||
this.filteredData = [];
|
||||
this.search = '';
|
||||
this.menuCategory = '';
|
||||
}
|
||||
|
||||
connect(): Observable<Bundle[]> {
|
||||
const dataMutations = [
|
||||
this.dataObs.pipe(
|
||||
tap((x) => {
|
||||
this.data = x;
|
||||
}),
|
||||
),
|
||||
this.searchFilter.pipe(
|
||||
tap((x) => {
|
||||
this.search = x;
|
||||
}),
|
||||
),
|
||||
this.menuCategoryFilter.pipe(
|
||||
tap((x) => {
|
||||
this.menuCategory = x;
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
return merge(...dataMutations).pipe(
|
||||
map(() => this.getFilteredData(this.data, this.search, this.menuCategory)),
|
||||
tap((x) => {
|
||||
this.filteredData = x;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getFilteredData(data: Bundle[], search: string, menuCategory: string): Bundle[] {
|
||||
const tokens = (search ?? '').toLowerCase().split(/\s+/).filter(Boolean);
|
||||
|
||||
return data.filter((b: Bundle) => {
|
||||
const mcName = b.menuCategory?.name ?? '';
|
||||
const hay = `${b.name ?? ''} ${b.units ?? ''} ${mcName}`.toLowerCase();
|
||||
|
||||
const matchesSearch = tokens.length === 0 || tokens.every((t) => hay.includes(t));
|
||||
const matchesMenuCategory = menuCategory === '' || (b.menuCategory?.id ?? '') === menuCategory;
|
||||
|
||||
return matchesSearch && matchesMenuCategory;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Bundles</span>
|
||||
<button mat-icon-button (click)="exportCsv()" [disabled]="!(data | async)?.length">
|
||||
<button type="button" mat-icon-button (click)="exportCsv()" [disabled]="!dataSource().length">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<a mat-button [routerLink]="['/bundles', 'new']">
|
||||
@@ -9,18 +9,18 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input type="text" matInput formControlName="filter" autocomplete="off" />
|
||||
<input type="text" matInput [formField]="form.filter" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select formControlName="menuCategory" (selectionChange)="filterOn($event.value)">
|
||||
<mat-select [formField]="form.menuCategory" (selectionChange)="filterOn($event.value)">
|
||||
<mat-option>-- All Bundles --</mat-option>
|
||||
@for (mc of menuCategories; track mc) {
|
||||
@for (mc of menuCategoriesResource.value(); track mc) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Bundles">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Bundles">
|
||||
<!-- Name -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, inject, computed, signal, debounced } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -8,21 +8,23 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
import { MenuCategoryService } from '../../menu-category/menu-category.service';
|
||||
import { ToCsvService } from '../../shared/to-csv.service';
|
||||
import { Bundle } from '../bundle';
|
||||
import { BundleListDataSource } from './bundle-list-datasource';
|
||||
import { BundleService } from '../bundle.service';
|
||||
|
||||
export interface BundleListFormData {
|
||||
menuCategory: string | null;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-list',
|
||||
templateUrl: './bundle-list.component.html',
|
||||
styleUrls: ['./bundle-list.component.css'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
@@ -31,58 +33,44 @@ import { BundleListDataSource } from './bundle-list-datasource';
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class BundleListComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class BundleListComponent {
|
||||
private menuCategoryService = inject(MenuCategoryService);
|
||||
private bundleService = inject(BundleService);
|
||||
private toCsv = inject(ToCsvService);
|
||||
|
||||
searchFilter = new BehaviorSubject<string>('');
|
||||
menuCategoryFilter = new BehaviorSubject<string>('');
|
||||
data: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||
dataSource: BundleListDataSource = new BundleListDataSource(this.searchFilter, this.menuCategoryFilter, this.data);
|
||||
menuCategoriesResource = this.menuCategoryService.list(undefined);
|
||||
listResource = this.bundleService.list();
|
||||
|
||||
form: FormGroup<{
|
||||
menuCategory: FormControl<string | null>;
|
||||
filter: FormControl<string>;
|
||||
}>;
|
||||
menuCategoryFilter = signal<string>('');
|
||||
|
||||
formModel = signal<BundleListFormData>({
|
||||
menuCategory: '',
|
||||
filter: '',
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
filterSignal = computed(() => this.formModel().filter);
|
||||
debouncedFilter = debounced(this.filterSignal, 150);
|
||||
|
||||
dataSource = computed(() => {
|
||||
let items = this.listResource.value() ?? [];
|
||||
const search = this.debouncedFilter.value()?.toLowerCase() ?? '';
|
||||
const cat = this.menuCategoryFilter();
|
||||
|
||||
if (search) items = items.filter((x: Bundle) => x.name.toLowerCase().includes(search));
|
||||
if (cat) items = items.filter((x: Bundle) => x.menuCategory?.id === cat);
|
||||
return items;
|
||||
});
|
||||
|
||||
list: Bundle[] = [];
|
||||
menuCategories: MenuCategory[] = [];
|
||||
displayedColumns: string[] = ['name', 'price', 'menuCategory', 'saleCategory', 'info', 'items'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
menuCategory: new FormControl<string | null>(''),
|
||||
filter: new FormControl<string>('', { nonNullable: true }),
|
||||
});
|
||||
|
||||
this.data.subscribe((data: Bundle[]) => {
|
||||
this.list = data;
|
||||
});
|
||||
|
||||
this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged()).subscribe((value) => {
|
||||
this.searchFilter.next(value ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
filterOn(val: string) {
|
||||
this.menuCategoryFilter.next(val);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource = new BundleListDataSource(this.searchFilter, this.menuCategoryFilter, this.data);
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Bundle[]; menuCategories: MenuCategory[] };
|
||||
this.loadData(data.list, data.menuCategories);
|
||||
});
|
||||
}
|
||||
|
||||
loadData(list: Bundle[], menuCategories: MenuCategory[]) {
|
||||
this.menuCategories = menuCategories;
|
||||
this.data.next(list);
|
||||
this.menuCategoryFilter.set(val);
|
||||
}
|
||||
|
||||
exportCsv() {
|
||||
@@ -96,7 +84,7 @@ export class BundleListComponent implements OnInit {
|
||||
};
|
||||
|
||||
// Map items to count for csv (simple)
|
||||
const csvList = this.dataSource.filteredData.map((b) => ({
|
||||
const csvList = this.dataSource().map((b: Bundle) => ({
|
||||
...b,
|
||||
menuCategory: b.menuCategory?.name ?? '',
|
||||
items: (b.items ?? [])
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleResolver } from './bundle-resolver.service';
|
||||
|
||||
describe('BundleResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleResolver], (service: BundleResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
|
||||
|
||||
import { Bundle } from './bundle';
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
export const bundleResolver: ResolveFn<Bundle> = (route: ActivatedRouteSnapshot) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(BundleService).get(id);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -18,17 +18,15 @@ export class BundleService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<Bundle[]> {
|
||||
return this.http
|
||||
.get<Bundle[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Bundle[]>;
|
||||
list(): HttpResourceRef<Bundle[] | undefined> {
|
||||
return httpResource<Bundle[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
get(id: string | null): Observable<Bundle> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Bundle>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Bundle>;
|
||||
get(id: Signal<string | null>): HttpResourceRef<Bundle | undefined> {
|
||||
return httpResource<Bundle>(() => {
|
||||
const idVal = id();
|
||||
return idVal === null ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
save(bundle: Bundle): Observable<void> {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
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';
|
||||
import { bundleResolver } from './bundle.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,31 +10,17 @@ export const routes: Routes = [
|
||||
component: BundleListComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' }, // change if you have a separate permission
|
||||
resolve: {
|
||||
list: bundleListResolver,
|
||||
menuCategories: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: BundleDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' },
|
||||
resolve: {
|
||||
item: bundleResolver, // returns new Bundle() because id === null (no param here)
|
||||
menuCategories: menuCategoryListResolver,
|
||||
saleCategories: saleCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: BundleDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' },
|
||||
resolve: {
|
||||
item: bundleResolver,
|
||||
menuCategories: menuCategoryListResolver,
|
||||
saleCategories: saleCategoryListResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActiveCashiersResolver } from './active-cashiers-resolver.service';
|
||||
|
||||
describe('ActiveCashiersResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ActiveCashiersResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ActiveCashiersResolver], (service: ActiveCashiersResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { User } from '../core/user';
|
||||
import { CashierReportService } from './cashier-report.service';
|
||||
|
||||
export const activeCashiersResolver: ResolveFn<User[]> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(CashierReportService).activeCashiers(startDate, finishDate);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { CashierReportDisplayItem } from './cashier-report-display-item';
|
||||
|
||||
export class CashierReportDataSource extends DataSource<CashierReportDisplayItem> {
|
||||
constructor(public data: CashierReportDisplayItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<CashierReportDisplayItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Cashier Report</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button [disabled]="!info.cashier.id" (click)="exportCsv()">
|
||||
<button type="button" mat-icon-button [disabled]="!info().cashier.id" (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [disabled]="!info.cashier.id" (click)="print()">
|
||||
<button type="button" mat-icon-button [disabled]="!info().cashier.id" (click)="print()">
|
||||
<mat-icon>print</mat-icon>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Start Date</mat-label>
|
||||
@@ -17,7 +17,7 @@
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
formControlName="startDate"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
@@ -29,7 +29,7 @@
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
@@ -39,19 +39,19 @@
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-4-5">
|
||||
<mat-label>Cashier</mat-label>
|
||||
<mat-select formControlName="cashier">
|
||||
<mat-select [formField]="form.cashier">
|
||||
<mat-option>-- Cashier --</mat-option>
|
||||
@for (ac of activeCashiers; track ac) {
|
||||
@for (ac of cashiers(); track ac) {
|
||||
<mat-option [value]="ac.id">
|
||||
{{ ac.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
<button type="button" mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
@@ -16,9 +16,14 @@ import moment from 'moment';
|
||||
import { User } from '../core/user';
|
||||
import { ToCsvService } from '../shared/to-csv.service';
|
||||
import { CashierReport } from './cashier-report';
|
||||
import { CashierReportDataSource } from './cashier-report-datasource';
|
||||
import { CashierReportService } from './cashier-report.service';
|
||||
|
||||
export interface CashierReportFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
cashier: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-cashier-report',
|
||||
templateUrl: './cashier-report.component.html',
|
||||
@@ -26,7 +31,6 @@ import { CashierReportService } from './cashier-report.service';
|
||||
imports: [
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
@@ -34,51 +38,49 @@ import { CashierReportService } from './cashier-report.service';
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class CashierReportComponent implements OnInit {
|
||||
export class CashierReportComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private toCsv = inject(ToCsvService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(CashierReportService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
activeCashiers: User[] = [];
|
||||
info: CashierReport = new CashierReport();
|
||||
dataSource: CashierReportDataSource = new CashierReportDataSource(this.info.amounts);
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
cashier: FormControl<string | null>;
|
||||
}>;
|
||||
infoResource = this.ser.list(this.id, this.startDate, this.finishDate);
|
||||
cashiersResource = this.ser.activeCashiers(this.startDate, this.finishDate);
|
||||
|
||||
cashiers = computed(() => (this.cashiersResource.value() ?? []) as User[]);
|
||||
|
||||
info = computed(() => this.infoResource.value() ?? new CashierReport());
|
||||
dataSource = computed(() => this.info().amounts);
|
||||
formModel = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (infoVal): CashierReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
cashier: this.info().cashier.id ?? '',
|
||||
startDate: moment(this.info().startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info().finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
startDate: new Date(),
|
||||
finishDate: new Date(),
|
||||
cashier: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'amount'];
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
cashier: new FormControl<string | null>(null),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { cashiers: User[]; info: CashierReport };
|
||||
this.activeCashiers = data.cashiers;
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
cashier: this.info.cashier.id ?? '',
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
});
|
||||
this.dataSource = new CashierReportDataSource(this.info.amounts);
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
const url = ['cashier-report'];
|
||||
@@ -94,22 +96,22 @@ export class CashierReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
print() {
|
||||
this.ser.print(this.info.cashier.id as string, this.info.startDate, this.info.finishDate).subscribe({
|
||||
this.ser.print(this.info().cashier.id as string, this.info().startDate, this.info().finishDate).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Successfully Printed', '');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getInfo(): CashierReport {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new CashierReport({
|
||||
cashier: new User({ id: formModel.cashier ?? undefined }),
|
||||
cashiers: this.info.cashiers,
|
||||
cashiers: this.info().cashiers,
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
|
||||
});
|
||||
@@ -120,7 +122,7 @@ export class CashierReportComponent implements OnInit {
|
||||
Name: 'name',
|
||||
Amount: 'amount',
|
||||
};
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource())], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CashierReportResolver } from './cashier-report-resolver.service';
|
||||
|
||||
describe('CashierReportResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [CashierReportResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CashierReportResolver], (service: CashierReportResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { CashierReport } from './cashier-report';
|
||||
import { CashierReportService } from './cashier-report.service';
|
||||
|
||||
export const cashierReportResolver: ResolveFn<CashierReport> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(CashierReportService).list(id, startDate, finishDate);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { activeCashiersResolver } from './active-cashiers.resolver';
|
||||
import { CashierReportComponent } from './cashier-report.component';
|
||||
import { cashierReportResolver } from './cashier-report.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cashier Report',
|
||||
},
|
||||
resolve: {
|
||||
info: cashierReportResolver,
|
||||
cashiers: activeCashiersResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cashier Report',
|
||||
},
|
||||
resolve: {
|
||||
info: cashierReportResolver,
|
||||
cashiers: activeCashiersResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -17,32 +17,43 @@ export class CashierReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(id: string | null, startDate: string | null, finishDate: string | null): Observable<CashierReport> {
|
||||
const listUrl = id === null ? url : `${url}/${id}`;
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
return this.http
|
||||
.get<CashierReport>(listUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<CashierReport>;
|
||||
list(
|
||||
id: Signal<string | null>,
|
||||
startDate: Signal<string | null>,
|
||||
finishDate: Signal<string | null>,
|
||||
): HttpResourceRef<CashierReport | undefined> {
|
||||
return httpResource<CashierReport>(() => {
|
||||
const idVal = id();
|
||||
const listUrl = idVal === null ? url : `${url}/${idVal}`;
|
||||
let params = new HttpParams();
|
||||
const s = startDate();
|
||||
const f = finishDate();
|
||||
if (s !== null) {
|
||||
params = params.set('s', s);
|
||||
}
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return { url: listUrl, params };
|
||||
});
|
||||
}
|
||||
|
||||
activeCashiers(startDate: string | null, finishDate: string | null): Observable<User[]> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
return this.http
|
||||
.get<User[]>(`${url}/active`, options)
|
||||
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'activeCashiers'))) as Observable<User[]>;
|
||||
activeCashiers(
|
||||
startDate: Signal<string | null>,
|
||||
finishDate: Signal<string | null>,
|
||||
): HttpResourceRef<User[] | undefined> {
|
||||
return httpResource<User[]>(() => {
|
||||
let params = new HttpParams();
|
||||
const s = startDate();
|
||||
const f = finishDate();
|
||||
if (s !== null) {
|
||||
params = params.set('s', s);
|
||||
}
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return { url: `${url}/active`, params };
|
||||
});
|
||||
}
|
||||
|
||||
print(id: string, startDate: string | null, finishDate: string | null): Observable<boolean> {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export class UserRole {
|
||||
id: string | undefined;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
|
||||
public constructor(init?: Partial<UserRole>) {
|
||||
this.id = undefined;
|
||||
this.name = '';
|
||||
this.enabled = false;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,41 @@
|
||||
<h2>Customer</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" />
|
||||
<input matInput #nameElement [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" />
|
||||
<input matInput [formField]="form.phone" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Address</mat-label>
|
||||
<textarea matInput formControlName="address"> </textarea>
|
||||
<textarea matInput [formField]="form.address"> </textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="printInBill">Print in Bill?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.printInBill">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div formArrayName="discounts" class="discount-grid">
|
||||
@for (r of item.discounts; track r; let i = $index) {
|
||||
<mat-form-field [formGroupName]="i">
|
||||
<mat-label>Discount on {{ r.name }}</mat-label>
|
||||
<input matInput formControlName="discount" />
|
||||
<span matTextSuffix>%</span>
|
||||
<mat-hint>Max {{ r.limit | percent: '1.2-2' }}</mat-hint>
|
||||
</mat-form-field>
|
||||
<div class="discounts">
|
||||
@for (r of item().discounts; track r; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Discount on {{ r.name }}</mat-label>
|
||||
<input matInput type="number" [formField]="form.discounts[i].discount" />
|
||||
<span matTextSuffix>%</span>
|
||||
<mat-hint>Max {{ r.limit | percent: '1.2-2' }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
<button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PercentPipe } from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -15,6 +15,16 @@ import { Customer } from '../../core/customer';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { CustomerService } from '../customer.service';
|
||||
|
||||
export interface CustomerDetailFormData {
|
||||
name: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
printInBill: boolean;
|
||||
discounts: {
|
||||
discount: number | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-detail',
|
||||
templateUrl: './customer-detail.component.html',
|
||||
@@ -26,66 +36,37 @@ import { CustomerService } from '../customer.service';
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
PercentPipe,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class CustomerDetailComponent implements OnInit, AfterViewInit {
|
||||
export class CustomerDetailComponent implements AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private dialog = inject(MatDialog);
|
||||
private ser = inject(CustomerService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string>;
|
||||
phone: FormControl<string>;
|
||||
address: FormControl<string | null>;
|
||||
printInBill: FormControl<boolean>;
|
||||
discounts: FormArray<
|
||||
FormGroup<{
|
||||
discount: FormControl<number | null>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
item: Customer = new Customer();
|
||||
hide: boolean;
|
||||
item = computed(() => this.itemResource.value() ?? new Customer());
|
||||
formModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): CustomerDetailFormData => {
|
||||
return {
|
||||
name: itemVal.name ?? '',
|
||||
phone: itemVal.phone ?? '',
|
||||
address: itemVal.address ?? '',
|
||||
printInBill: itemVal.printInBill ?? false,
|
||||
discounts: itemVal.discounts.map((x) => ({ discount: x.discount ? x.discount * 100 : null })),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.hide = true;
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
phone: new FormControl<string>('', { nonNullable: true }),
|
||||
address: new FormControl<string | null>(null),
|
||||
printInBill: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
discounts: new FormArray<FormGroup<{ discount: FormControl<number | null> }>>([]),
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: Customer };
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(item: Customer) {
|
||||
this.item = item;
|
||||
this.form.controls.name.setValue(item.name);
|
||||
this.form.controls.phone.setValue(item.phone);
|
||||
this.form.controls.address.setValue(item.address);
|
||||
this.form.controls.printInBill.setValue(item.printInBill);
|
||||
this.form.controls.discounts.clear();
|
||||
this.item.discounts.forEach((x) =>
|
||||
this.form.controls.discounts.push(
|
||||
new FormGroup({
|
||||
discount: new FormControl<number | null>(x.discount ? x.discount * 100 : null),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
hide = true;
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
@@ -101,20 +82,20 @@ export class CustomerDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/customers');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
this.ser.delete(this.item().id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/customers');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -133,23 +114,20 @@ export class CustomerDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Customer {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.phone = formModel.phone ?? '';
|
||||
this.item.address = formModel.address ?? null;
|
||||
this.item.printInBill = formModel.printInBill ?? false;
|
||||
|
||||
const array = formModel.discounts;
|
||||
if (array) {
|
||||
this.item.discounts.forEach((item, index) => {
|
||||
const array_item = array.at(index);
|
||||
if (array_item && array_item?.discount) {
|
||||
item.discount = Math.max(Math.min(round(array_item.discount / 100, 5), 100), 0);
|
||||
} else {
|
||||
item.discount = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.item;
|
||||
const formModel = this.formModel();
|
||||
return new Customer({
|
||||
...this.item(),
|
||||
name: formModel.name ?? '',
|
||||
phone: formModel.phone ?? '',
|
||||
address: formModel.address ?? null,
|
||||
printInBill: formModel.printInBill ?? false,
|
||||
discounts: this.item().discounts.map((item, index) => {
|
||||
const discount = formModel.discounts[index]?.discount;
|
||||
return {
|
||||
...item,
|
||||
discount: discount ? Math.max(Math.min(round(discount / 100, 5), 100), 0) : 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CustomerListResolver } from './customer-list-resolver.service';
|
||||
|
||||
describe('CustomerListResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [CustomerListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CustomerListResolver], (service: CustomerListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Customer } from '../core/customer';
|
||||
import { PagedResult } from '../core/paged-result';
|
||||
import { CustomerService } from './customer.service';
|
||||
|
||||
export const customerListResolver: ResolveFn<PagedResult<Customer>> = (route) => {
|
||||
const q = route.queryParamMap.get('q');
|
||||
const page = Number(route.queryParamMap.get('p') ?? 0);
|
||||
const size = Number(route.queryParamMap.get('s') ?? 50);
|
||||
const field = route.queryParamMap.get('f') ?? 'name';
|
||||
const direction = route.queryParamMap.get('d') ?? 'asc';
|
||||
return inject(CustomerService).list(q, page, size, field, direction);
|
||||
};
|
||||
@@ -1,161 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { Customer } from '../../core/customer';
|
||||
import { PagedResult } from '../../core/paged-result';
|
||||
|
||||
export class CustomerListDatasource extends DataSource<Customer> {
|
||||
constructor(
|
||||
public data: PagedResult<Customer>,
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Customer[]> {
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.total;
|
||||
if (this.data.pageSize !== undefined) {
|
||||
this.paginator.pageSize = this.data.pageSize;
|
||||
}
|
||||
if (this.data.page !== undefined) {
|
||||
this.paginator.pageIndex = this.data.page;
|
||||
}
|
||||
}
|
||||
if (this.sort) {
|
||||
if (this.data.field !== undefined) {
|
||||
this.sort.active = this.data.field;
|
||||
}
|
||||
if (this.data.sortDirection !== undefined) {
|
||||
this.sort.direction = this.data.sortDirection;
|
||||
}
|
||||
}
|
||||
return observableOf(this.data.items);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
// import { DataSource } from '@angular/cdk/collections';
|
||||
// import { map, merge, Observable, of as observableOf, tap } from 'rxjs';
|
||||
|
||||
// import { Customer } from '../../core/customer';
|
||||
// import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
// import { MatSort, Sort } from '@angular/material/sort';
|
||||
// import { EventEmitter } from '@angular/core';
|
||||
|
||||
// export class CustomerListDatasource extends DataSource<Customer> {
|
||||
// private filterValue = '';
|
||||
// constructor(public data: Customer[],
|
||||
// private readonly filter: Observable<string>,
|
||||
// private paginator?: MatPaginator,
|
||||
// private sort?: MatSort,
|
||||
|
||||
// ) {
|
||||
// super();
|
||||
// this.filter = filter.pipe(
|
||||
// tap((x) => {
|
||||
// this.filterValue = x;
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// connect(): Observable<Customer[]> {
|
||||
// const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
|
||||
// if (this.paginator) {
|
||||
// dataMutations.push((this.paginator as MatPaginator).page);
|
||||
// }
|
||||
// if (this.sort) {
|
||||
// dataMutations.push((this.sort as MatSort).sortChange);
|
||||
// }
|
||||
|
||||
// return merge(observableOf(this.data), this.filter, ...dataMutations).pipe(
|
||||
// map(() => this.getFilteredData([...this.data])),
|
||||
// tap((x: Customer[]) => {
|
||||
// if (this.paginator) {
|
||||
// this.paginator.length = x.length;
|
||||
// }
|
||||
// }),
|
||||
// map((x: Customer[]) => this.getPagedData(this.getSortedData(x))),
|
||||
// );
|
||||
// }
|
||||
|
||||
// disconnect() {}
|
||||
// private getFilteredData(data: Customer[]): Customer[] {
|
||||
// return this.filterValue.toLowerCase().split(' ').reduce(
|
||||
// (p: Customer[], c: string) =>
|
||||
// p.filter((x) => {
|
||||
// const productString = `${x.name} ${x.phone}${x.address}`.toLowerCase();
|
||||
// return productString.indexOf(c) !== -1;
|
||||
// }),
|
||||
// Object.assign([], data),
|
||||
// );
|
||||
// }
|
||||
// private getPagedData(data: Customer[]) {
|
||||
// if (this.paginator === undefined) {
|
||||
// return data;
|
||||
// }
|
||||
// const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
// return data.splice(startIndex, this.paginator.pageSize);
|
||||
// }
|
||||
|
||||
// private getSortedData(data: Customer[]) {
|
||||
// if (this.sort === undefined) {
|
||||
// return data;
|
||||
// }
|
||||
// if (!this.sort.active || this.sort.direction === '') {
|
||||
// return data;
|
||||
// }
|
||||
|
||||
// const sort = this.sort as MatSort;
|
||||
// console.log('Sorting by', sort.active, sort.direction);
|
||||
// return data.sort(sortBy(sort.active as keyof Customer, sort.direction === 'asc'));
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// function compareValues<T>(a: T, b: T, isAsc: boolean): number {
|
||||
// if (a === b) return 0;
|
||||
// if (a == null) return isAsc ? -1 : 1;
|
||||
// if (b == null) return isAsc ? 1 : -1;
|
||||
|
||||
// if (typeof a === "string" && typeof b === "string") {
|
||||
// return isAsc ? a.localeCompare(b) : b.localeCompare(a);
|
||||
// }
|
||||
// if (typeof a === "number" && typeof b === "number") {
|
||||
// return isAsc ? a - b : b - a;
|
||||
// }
|
||||
// if (typeof a === "boolean" && typeof b === "boolean") {
|
||||
// return isAsc ? (Number(a) - Number(b)) : (Number(b) - Number(a));
|
||||
// }
|
||||
// // Fallback
|
||||
// const aStr = String(a);
|
||||
// const bStr = String(b);
|
||||
// return isAsc ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
|
||||
// }
|
||||
|
||||
// function sortBy<T, K extends keyof T>(
|
||||
// key: K,
|
||||
// isAsc = true
|
||||
// ): (a: T, b: T) => number {
|
||||
// return (a, b) => compareValues(a[key], b[key], isAsc);
|
||||
// }
|
||||
|
||||
// // function getNestedValue<T>(obj: T, path: string | string[]): any {
|
||||
// // const keys = Array.isArray(path) ? path : path.split(".");
|
||||
// // return keys.reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
||||
// // }
|
||||
|
||||
// // function sortByNested<T>(
|
||||
// // path: string | string[],
|
||||
// // isAsc = true
|
||||
// // ): (a: T, b: T) => number {
|
||||
// // return (a, b) => {
|
||||
// // const aValue = getNestedValue(a, path);
|
||||
// // const bValue = getNestedValue(b, path);
|
||||
// // return compareValues(aValue, bValue, isAsc);
|
||||
// // };
|
||||
// // }
|
||||
@@ -5,15 +5,15 @@
|
||||
Add
|
||||
</a>
|
||||
</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" />
|
||||
<input type="text" matInput #filterElement [formField]="form.filter" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource().items" matSort aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
@@ -57,7 +57,7 @@
|
||||
</mat-table>
|
||||
<mat-paginator
|
||||
#paginator
|
||||
[length]="dataSource.data.total"
|
||||
[length]="dataSource().total"
|
||||
[pageIndex]="0"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 5000]"
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { PercentPipe } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild, inject, AfterViewInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
inject,
|
||||
signal,
|
||||
effect,
|
||||
computed,
|
||||
debounced,
|
||||
} from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
@@ -8,11 +18,13 @@ import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { debounceTime, distinctUntilChanged, Observable } from 'rxjs';
|
||||
|
||||
import { Customer } from '../../core/customer';
|
||||
import { PagedResult } from '../../core/paged-result';
|
||||
import { CustomerListDatasource } from './customer-list-datasource';
|
||||
import { CustomerService } from '../customer.service';
|
||||
|
||||
export interface CustomerListFormData {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-list',
|
||||
@@ -20,7 +32,6 @@ import { CustomerListDatasource } from './customer-list-datasource';
|
||||
styleUrls: ['./customer-list.component.css'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
PercentPipe,
|
||||
@@ -28,76 +39,75 @@ import { CustomerListDatasource } from './customer-list-datasource';
|
||||
MatInputModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class CustomerListComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
export class CustomerListComponent implements AfterViewInit {
|
||||
@ViewChild('filterElement', { static: true }) filterElement!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private customerService = inject(CustomerService);
|
||||
|
||||
data: PagedResult<Customer> = new PagedResult<Customer>([], 0, 0, 0);
|
||||
filter: Observable<string>;
|
||||
dataSource: CustomerListDatasource;
|
||||
form: FormGroup<{
|
||||
filter: FormControl<string>;
|
||||
}>;
|
||||
formModel = signal<CustomerListFormData>({
|
||||
filter: this.route.snapshot.queryParamMap.get('q') ?? '',
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
filterSignal = computed(() => this.formModel().filter);
|
||||
debouncedFilter = debounced(this.filterSignal, 150);
|
||||
|
||||
pageIndex = signal(+(this.route.snapshot.queryParamMap.get('p') ?? '0'));
|
||||
pageSize = signal(+(this.route.snapshot.queryParamMap.get('s') ?? '50'));
|
||||
sortField = signal(this.route.snapshot.queryParamMap.get('f') ?? 'name');
|
||||
sortDirection = signal(this.route.snapshot.queryParamMap.get('d') ?? 'asc');
|
||||
|
||||
queryParams = computed(() => ({
|
||||
q: this.debouncedFilter.value() ?? '',
|
||||
p: this.pageIndex(),
|
||||
s: this.pageSize(),
|
||||
f: this.sortField(),
|
||||
d: this.sortDirection(),
|
||||
}));
|
||||
|
||||
listResource = this.customerService.query(this.queryParams);
|
||||
|
||||
dataSource = computed(() => {
|
||||
const data = this.listResource.value();
|
||||
if (!data) return { items: [], total: 0 };
|
||||
data.items.forEach((c: Customer) => (c.discounts = c.discounts.filter((d) => d.discount !== 0)));
|
||||
return data;
|
||||
});
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
filter: new FormControl<string>('', { nonNullable: true }),
|
||||
});
|
||||
this.filter = this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged());
|
||||
this.dataSource = new CustomerListDatasource(this.data, this.paginator, this.sort);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const q = this.route.snapshot.queryParamMap.get('q');
|
||||
if (q) {
|
||||
this.form.controls.filter.setValue(q, { emitEvent: false }); // Prevent infinite loop
|
||||
}
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { data: PagedResult<Customer> };
|
||||
data.data.items.forEach((c: Customer) => (c.discounts = c.discounts.filter((d) => d.discount !== 0)));
|
||||
this.data = data.data;
|
||||
this.dataSource = new CustomerListDatasource(this.data, this.paginator, this.sort);
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
this.sort.sortChange.subscribe({
|
||||
next: () =>
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { f: this.sort?.active ?? 'name', d: this.sort?.direction },
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
});
|
||||
this.paginator.page.subscribe({
|
||||
next: () =>
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { s: this.paginator.pageSize, p: this.paginator.pageIndex },
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
});
|
||||
this.filter.subscribe({
|
||||
next: (value) => {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { q: value },
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
},
|
||||
// Sync query string whenever params change
|
||||
effect(() => {
|
||||
const p = this.queryParams();
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { q: p.q, p: p.p, s: p.s, f: p.f, d: p.d },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.sort.sortChange.subscribe(() => {
|
||||
this.sortField.set(this.sort.active);
|
||||
this.sortDirection.set(this.sort.direction);
|
||||
this.pageIndex.set(0);
|
||||
});
|
||||
|
||||
this.paginator.page.subscribe(() => {
|
||||
this.pageSize.set(this.paginator.pageSize);
|
||||
this.pageIndex.set(this.paginator.pageIndex);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.filterElement.nativeElement.focus();
|
||||
}, 0);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CustomerResolver } from './customer-resolver.service';
|
||||
|
||||
describe('CustomerResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [CustomerResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CustomerResolver], (service: CustomerResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Customer } from '../core/customer';
|
||||
import { CustomerService } from './customer.service';
|
||||
|
||||
export const customerResolver: ResolveFn<Customer> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(CustomerService).get(id);
|
||||
};
|
||||
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { CustomerDetailComponent } from './customer-detail/customer-detail.component';
|
||||
import { customerListResolver } from './customer-list.resolver';
|
||||
import { CustomerListComponent } from './customer-list/customer-list.component';
|
||||
import { customerResolver } from './customer.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
data: customerListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,16 +21,10 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
item: customerResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: CustomerDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
resolve: {
|
||||
item: customerResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -20,11 +20,11 @@ export class CustomerService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<Customer> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Customer>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Customer>;
|
||||
get(id: Signal<string | null> | string | null): HttpResourceRef<Customer | undefined> {
|
||||
return httpResource<Customer>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
return idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
list(q: string | null, page = 0, size = 50, field = 'name', direction = 'asc'): Observable<PagedResult<Customer>> {
|
||||
@@ -35,12 +35,29 @@ export class CustomerService {
|
||||
f: field,
|
||||
d: direction,
|
||||
};
|
||||
console.log('CustomerService.list', params);
|
||||
return this.http
|
||||
.get<PagedResult<Customer>>(`${url}/list`, { params })
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<PagedResult<Customer>>;
|
||||
}
|
||||
|
||||
query(
|
||||
params: Signal<{ q: string; p: number; s: number; f: string; d: string }>,
|
||||
): HttpResourceRef<PagedResult<Customer> | undefined> {
|
||||
return httpResource<PagedResult<Customer>>(() => {
|
||||
const p = params();
|
||||
return {
|
||||
url: `${url}/list`,
|
||||
params: {
|
||||
...(p.q ? { q: p.q } : {}),
|
||||
p: p.p,
|
||||
s: p.s,
|
||||
f: p.f,
|
||||
d: p.d,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
save(customer: Customer): Observable<Customer> {
|
||||
return this.http
|
||||
.post<Customer>(`${url}`, customer, httpOptions)
|
||||
@@ -66,10 +83,10 @@ export class CustomerService {
|
||||
.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[]>;
|
||||
autocomplete(query: Signal<string | null> | string | null): HttpResourceRef<Customer[] | undefined> {
|
||||
return httpResource<Customer[]>(() => {
|
||||
const qVal = typeof query === 'function' ? query() : query;
|
||||
return `${url}/query?q=${qVal ?? ''}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<h2>Device</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" (keyup.enter)="save()" />
|
||||
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Section</mat-label>
|
||||
<mat-select formControlName="section">
|
||||
@for (s of sections; track s) {
|
||||
<mat-select [formField]="form.section">
|
||||
@for (s of sections(); track s) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
@@ -19,16 +19,16 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="enabled" class="flex-auto">Enabled?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.enabled" class="flex-auto">Enabled?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container space-between">
|
||||
<button mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item().id) {
|
||||
<button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
<div class="spacer"></div>
|
||||
<span
|
||||
>Created on <strong>{{ item.creationDate | localTime }}</strong></span
|
||||
>Created on <strong>{{ item().creationDate | localTime }}</strong></span
|
||||
>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
@@ -12,10 +12,17 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Device } from '../../core/device';
|
||||
import { Section } from '../../core/section';
|
||||
import { SectionService } from '../../sections/section.service';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { DeviceService } from '../device.service';
|
||||
|
||||
export interface DeviceDetailFormData {
|
||||
name: string;
|
||||
section: string | null;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-device-detail',
|
||||
templateUrl: './device-detail.component.html',
|
||||
@@ -23,58 +30,43 @@ import { DeviceService } from '../device.service';
|
||||
imports: [
|
||||
LocalTimePipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class DeviceDetailComponent implements OnInit, AfterViewInit {
|
||||
export class DeviceDetailComponent implements AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
private sectionService = inject(SectionService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(DeviceService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string>;
|
||||
section: FormControl<string | null>;
|
||||
enabled: FormControl<boolean>;
|
||||
}>;
|
||||
sectionsResource = this.sectionService.list();
|
||||
|
||||
sections: Section[] = [];
|
||||
item: Device = new Device();
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
section: new FormControl<string | null>(null),
|
||||
enabled: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
item = computed(() => this.itemResource.value() ?? new Device());
|
||||
formModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): DeviceDetailFormData => {
|
||||
return {
|
||||
name: itemVal.name ?? '',
|
||||
section: itemVal.section.id ? itemVal.section.id : '',
|
||||
enabled: itemVal.enabled,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: Device; sections: Section[] };
|
||||
this.showItem(data.item);
|
||||
this.sections = data.sections;
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
showItem(item: Device) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
name: this.item.name ?? '',
|
||||
section: this.item.section.id ? this.item.section.id : '',
|
||||
enabled: this.item.enabled,
|
||||
});
|
||||
}
|
||||
sections = computed(() => this.sectionsResource.value() ?? []);
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
@@ -90,20 +82,20 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/devices');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
this.ser.delete(this.item().id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/devices');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -122,10 +114,12 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Device {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.section.id = formModel.section ?? '';
|
||||
this.item.enabled = formModel.enabled ?? true;
|
||||
return this.item;
|
||||
const formModel = this.formModel();
|
||||
return new Device({
|
||||
...this.item(),
|
||||
name: formModel.name ?? '',
|
||||
section: new Section({ id: formModel.section ?? '' }),
|
||||
enabled: formModel.enabled ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DeviceListResolver } from './device-list-resolver.service';
|
||||
|
||||
describe('DeviceListResolverService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DeviceListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DeviceListResolver], (service: DeviceListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Device } from '../core/device';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
export const deviceListResolver: ResolveFn<Device[]> = () => {
|
||||
return inject(DeviceService).list();
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { Device } from '../../core/device';
|
||||
|
||||
export class DeviceListDataSource extends DataSource<Device> {
|
||||
constructor(public data: Device[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Device[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
Add
|
||||
</a>
|
||||
</h2>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
import { Device } from '../../core/device';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { DeviceListDataSource } from './device-list-datasource';
|
||||
|
||||
import { DeviceService } from '../device.service';
|
||||
@Component({
|
||||
selector: 'app-device-list',
|
||||
templateUrl: './device-list.component.html',
|
||||
styleUrls: ['./device-list.component.css'],
|
||||
imports: [LocalTimePipe, MatButtonModule, MatIconModule, MatTableModule, RouterLink],
|
||||
})
|
||||
export class DeviceListComponent implements OnInit {
|
||||
export class DeviceListComponent {
|
||||
private deviceService = inject(DeviceService);
|
||||
private route = inject(ActivatedRoute);
|
||||
listResource = this.deviceService.list();
|
||||
|
||||
list: Device[] = [];
|
||||
dataSource: DeviceListDataSource = new DeviceListDataSource(this.list);
|
||||
dataSource = computed(() => this.listResource.value() ?? []);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'section', 'enabled', 'creationDate', 'last'];
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Device[] };
|
||||
this.list = data.list;
|
||||
});
|
||||
this.dataSource = new DeviceListDataSource(this.list);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DeviceResolver } from './device-resolver.service';
|
||||
|
||||
describe('DeviceResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DeviceResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DeviceResolver], (service: DeviceResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Device } from '../core/device';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
export const deviceResolver: ResolveFn<Device> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(DeviceService).get(id);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -16,17 +16,15 @@ export class DeviceService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<Device> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Device>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Device>;
|
||||
get(id: Signal<string | null> | string | null): HttpResourceRef<Device | undefined> {
|
||||
return httpResource<Device>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
return idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Observable<Device[]> {
|
||||
return this.http
|
||||
.get<Device[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Device[]>;
|
||||
list(): HttpResourceRef<Device[] | undefined> {
|
||||
return httpResource<Device[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
save(device: Device): Observable<Device> {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { sectionListResolver } from '../sections/section-list.resolver';
|
||||
import { DeviceDetailComponent } from './device-detail/device-detail.component';
|
||||
import { deviceListResolver } from './device-list.resolver';
|
||||
import { DeviceListComponent } from './device-list/device-list.component';
|
||||
import { deviceResolver } from './device.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -15,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Devices',
|
||||
},
|
||||
resolve: {
|
||||
list: deviceListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Devices',
|
||||
},
|
||||
resolve: {
|
||||
item: deviceResolver,
|
||||
sections: sectionListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -38,9 +28,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Devices',
|
||||
},
|
||||
resolve: {
|
||||
item: deviceResolver,
|
||||
sections: sectionListResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { DiscountReportItem } from './discount-report-item';
|
||||
|
||||
export class DiscountReportDataSource extends DataSource<DiscountReportItem> {
|
||||
constructor(public data: DiscountReportItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<DiscountReportItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Discount Report</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button (click)="exportCsv()">
|
||||
<button type="button" mat-icon-button (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="print()">
|
||||
<button type="button" mat-icon-button (click)="print()">
|
||||
<mat-icon>print</mat-icon>
|
||||
</button>
|
||||
</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
@@ -16,7 +16,7 @@
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
formControlName="startDate"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
@@ -28,16 +28,16 @@
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
<button type="button" mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -13,9 +13,13 @@ import moment from 'moment';
|
||||
|
||||
import { ToCsvService } from '../shared/to-csv.service';
|
||||
import { DiscountReport } from './discount-report';
|
||||
import { DiscountReportDataSource } from './discount-report-datasource';
|
||||
import { DiscountReportService } from './discount-report.service';
|
||||
|
||||
export interface DiscountReportFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-discount-report',
|
||||
templateUrl: './discount-report.component.html',
|
||||
@@ -23,52 +27,48 @@ import { DiscountReportService } from './discount-report.service';
|
||||
imports: [
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class DiscountReportComponent implements OnInit {
|
||||
export class DiscountReportComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private toCsv = inject(ToCsvService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(DiscountReportService);
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
info: DiscountReport = new DiscountReport();
|
||||
dataSource: DiscountReportDataSource = new DiscountReportDataSource(this.info.amounts);
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
}>;
|
||||
infoResource = this.ser.get(this.startDate, this.finishDate);
|
||||
|
||||
info = computed(() => this.infoResource.value() ?? new DiscountReport());
|
||||
dataSource = computed(() => this.info().amounts);
|
||||
formModel = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (infoVal): DiscountReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: moment(this.info().startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info().finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
startDate: new Date(),
|
||||
finishDate: new Date(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'amount'];
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: DiscountReport };
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
});
|
||||
this.dataSource = new DiscountReportDataSource(this.info.amounts);
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
this.router.navigate(['discount-report'], {
|
||||
@@ -80,7 +80,7 @@ export class DiscountReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
getInfo(): DiscountReport {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new DiscountReport({
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
@@ -89,12 +89,12 @@ export class DiscountReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
print() {
|
||||
this.ser.print(this.info.startDate, this.info.finishDate).subscribe({
|
||||
this.ser.print(this.info().startDate, this.info().finishDate).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Successfully Printed', '');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export class DiscountReportComponent implements OnInit {
|
||||
Name: 'name',
|
||||
Amount: 'amount',
|
||||
};
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource())], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DiscountReportResolver } from './discount-report-resolver.service';
|
||||
|
||||
describe('DiscountReportResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DiscountReportResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DiscountReportResolver], (service: DiscountReportResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { DiscountReport } from './discount-report';
|
||||
import { DiscountReportService } from './discount-report.service';
|
||||
|
||||
export const discountReportResolver: ResolveFn<DiscountReport> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(DiscountReportService).get(startDate, finishDate);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { DiscountReportComponent } from './discount-report.component';
|
||||
import { discountReportResolver } from './discount-report.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Discount Report',
|
||||
},
|
||||
resolve: {
|
||||
info: discountReportResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -16,17 +16,22 @@ export class DiscountReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(startDate: string | null, finishDate: string | null): Observable<DiscountReport> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
return this.http
|
||||
.get<DiscountReport>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable<DiscountReport>;
|
||||
get(
|
||||
startDate: Signal<string | null>,
|
||||
finishDate: Signal<string | null>,
|
||||
): HttpResourceRef<DiscountReport | undefined> {
|
||||
return httpResource<DiscountReport>(() => {
|
||||
let params = new HttpParams();
|
||||
const s = startDate();
|
||||
const f = finishDate();
|
||||
if (s !== null) {
|
||||
params = params.set('s', s);
|
||||
}
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return { url, params };
|
||||
});
|
||||
}
|
||||
|
||||
print(startDate: string | null, finishDate: string | null): Observable<boolean> {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { GuestBook } from './guest-book';
|
||||
import { GuestBookService } from './guest-book.service';
|
||||
|
||||
export const guestBookDetailResolver: ResolveFn<GuestBook> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
const account = route.queryParamMap.get('t') || 'walk_in';
|
||||
return inject(GuestBookService).get(id, account);
|
||||
};
|
||||
@@ -1,18 +1,15 @@
|
||||
<h2>Guest Details</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #name formControlName="name" />
|
||||
@if (form.controls['name'].hasError('required')) {
|
||||
<mat-error> Name is <strong>required</strong> </mat-error>
|
||||
}
|
||||
<input matInput #name [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput #phone type="text" formControlName="phone" [matAutocomplete]="auto" autocomplete="off" />
|
||||
<input matInput #phone type="text" [formField]="form.phone" [matAutocomplete]="auto" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
@@ -20,7 +17,7 @@
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="selected($event)"
|
||||
>
|
||||
@for (customer of customers | async; track customer) {
|
||||
@for (customer of customers(); track customer) {
|
||||
<mat-option [value]="customer">{{ customer.name }} - {{ customer.phone }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@@ -28,42 +25,46 @@
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Pax</mat-label>
|
||||
<input matInput type="number" formControlName="pax" />
|
||||
<input matInput type="number" [formField]="form.pax" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Address</mat-label>
|
||||
<textarea matInput formControlName="address"></textarea>
|
||||
<textarea matInput [formField]="form.address"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Notes</mat-label>
|
||||
<textarea matInput formControlName="notes"></textarea>
|
||||
<textarea matInput [formField]="form.notes"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-1-2">
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput [matDatepicker]="date" (focus)="date.open()" formControlName="date" autocomplete="off" />
|
||||
<input matInput [matDatepicker]="date" (focus)="date.open()" [formField]="form.date" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-1-4">
|
||||
<mat-label>Hours</mat-label>
|
||||
<input matInput formControlName="hours" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-1-4">
|
||||
<mat-label>Minutes</mat-label>
|
||||
<input matInput formControlName="minutes" />
|
||||
<mat-form-field class="flex-auto basis-1-2">
|
||||
<mat-label>Time</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matTimepicker]="time"
|
||||
[formField]="form.time"
|
||||
matTimepickerMin="12:00"
|
||||
matTimepickerMax="23:45"
|
||||
/>
|
||||
<mat-timepicker-toggle matSuffix [for]="time"></mat-timepicker-toggle>
|
||||
<mat-timepicker #time interval="15min"></mat-timepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item().id) {
|
||||
<button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
</div>
|
||||
@@ -1,6 +1,15 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
computed,
|
||||
inject,
|
||||
debounced,
|
||||
linkedSignal,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatAutocompleteSelectedEvent, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
@@ -9,88 +18,98 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTimepickerModule } from '@angular/material/timepicker';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
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';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { GuestBook } from '../guest-book';
|
||||
import { GuestBook, GuestBookType } from '../guest-book';
|
||||
import { GuestBookService } from '../guest-book.service';
|
||||
|
||||
export interface GuestBookDetailFormData {
|
||||
name: string;
|
||||
phone: string | Customer;
|
||||
pax: number;
|
||||
address: string;
|
||||
notes: string;
|
||||
date: Date;
|
||||
time: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-guest-book-detail',
|
||||
templateUrl: './guest-book-detail.component.html',
|
||||
styleUrls: ['./guest-book-detail.component.css'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
|
||||
MatInputModule,
|
||||
MatOptionModule,
|
||||
ReactiveFormsModule,
|
||||
MatTimepickerModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class GuestBookDetailComponent implements OnInit, AfterViewInit {
|
||||
export class GuestBookDetailComponent implements AfterViewInit {
|
||||
@ViewChild('phone', { static: true }) phoneElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(GuestBookService);
|
||||
private customerService = inject(CustomerService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
t = input<GuestBookType, string | null | undefined>('walk_in', {
|
||||
transform: (v: string | null | undefined) => (v as GuestBookType) ?? 'walk_in',
|
||||
});
|
||||
|
||||
@ViewChild('phone', { static: true }) phoneElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string>;
|
||||
phone: FormControl<string | Customer>;
|
||||
pax: FormControl<number>;
|
||||
address: FormControl<string | null>;
|
||||
notes: FormControl<string | null>;
|
||||
date: FormControl<Date>;
|
||||
hours: FormControl<number>;
|
||||
minutes: FormControl<number>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id, this.t);
|
||||
|
||||
item: GuestBook = new GuestBook();
|
||||
customers: Observable<Customer[]>;
|
||||
item = computed(() => this.itemResource.value() ?? new GuestBook());
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
phone: new FormControl<string | Customer>('', {
|
||||
validators: Validators.required,
|
||||
nonNullable: true,
|
||||
}),
|
||||
pax: new FormControl(0, { validators: Validators.required, nonNullable: true }),
|
||||
address: new FormControl<string | null>(null),
|
||||
notes: new FormControl<string | null>(null),
|
||||
date: new FormControl({ value: new Date(), disabled: false }, { nonNullable: true }),
|
||||
hours: new FormControl(0, { validators: [Validators.min(0), Validators.max(23)], nonNullable: true }),
|
||||
minutes: new FormControl(0, { validators: [Validators.min(0), Validators.max(59)], nonNullable: true }),
|
||||
});
|
||||
// Setup Account Autocomplete
|
||||
this.customers = this.form.controls.phone.valueChanges.pipe(
|
||||
startWith(null),
|
||||
map((x) => (x !== null && (x as string).length >= 1 ? x : null)),
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.customerService.autocomplete(x as string))),
|
||||
);
|
||||
}
|
||||
formModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): GuestBookDetailFormData => {
|
||||
if (itemVal && itemVal.id) {
|
||||
const dt = (itemVal.arrivalDate || itemVal.bookingDate) as string;
|
||||
return {
|
||||
name: itemVal.name,
|
||||
phone: itemVal.phone,
|
||||
pax: itemVal.pax,
|
||||
address: itemVal.address ?? '',
|
||||
notes: itemVal.notes ?? '',
|
||||
date: dt ? new Date(dt) : new Date(),
|
||||
time: dt ? new Date(dt) : new Date(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: '',
|
||||
phone: '',
|
||||
pax: 0,
|
||||
address: '',
|
||||
notes: '',
|
||||
date: new Date(),
|
||||
time: new Date(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: GuestBook };
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
phoneSignal = computed(() => {
|
||||
const p = this.formModel().phone;
|
||||
return typeof p === 'string' ? p.trim() : (p as Customer).phone;
|
||||
});
|
||||
|
||||
debouncedPhone = debounced(this.phoneSignal, 150);
|
||||
|
||||
customersResource = this.customerService.autocomplete(computed(() => this.debouncedPhone.value()));
|
||||
customers = computed(() => {
|
||||
if (!this.debouncedPhone.value()) return [];
|
||||
return this.customersResource.value() ?? [];
|
||||
});
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
@@ -100,35 +119,14 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
showItem(item: GuestBook) {
|
||||
this.item = item;
|
||||
const dt = (this.item.arrivalDate || this.item.bookingDate) as string;
|
||||
if (item.type === 'booking') {
|
||||
this.form.controls.date.enable();
|
||||
} else {
|
||||
this.form.controls.date.disable();
|
||||
}
|
||||
|
||||
this.form.setValue({
|
||||
name: item.name,
|
||||
phone: item.phone,
|
||||
pax: item.pax,
|
||||
address: item.address,
|
||||
notes: item.notes,
|
||||
date: new Date(dt),
|
||||
hours: +dt.substring(12, 14),
|
||||
minutes: +dt.substring(15, 17),
|
||||
});
|
||||
}
|
||||
|
||||
save() {
|
||||
this.ser.saveOrUpdate(this.getItem()).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/guest-book');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -139,11 +137,12 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
|
||||
|
||||
selected(event: MatAutocompleteSelectedEvent): void {
|
||||
const customer = event.option.value;
|
||||
this.form.patchValue({
|
||||
this.formModel.update((f) => ({
|
||||
...f,
|
||||
name: customer.name,
|
||||
// phone: customer.phone,
|
||||
address: customer.address,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
confirmDelete(): void {
|
||||
@@ -154,7 +153,7 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
this.delete(this.item.id ?? '');
|
||||
this.delete(this.item().id ?? '');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -165,32 +164,36 @@ export class GuestBookDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/guest-book');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getItem(): GuestBook {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
const formModel = this.formModel();
|
||||
const item = Object.assign(new GuestBook(), this.item());
|
||||
item.name = formModel.name ?? '';
|
||||
if (typeof formModel.phone === 'string') {
|
||||
this.item.phone = formModel.phone;
|
||||
item.phone = formModel.phone;
|
||||
} else {
|
||||
this.item.phone = (formModel.phone as Customer).phone;
|
||||
item.phone = (formModel.phone as Customer).phone;
|
||||
}
|
||||
this.item.pax = formModel.pax ?? 0;
|
||||
this.item.address = formModel.address ?? null;
|
||||
this.item.notes = formModel.notes ?? null;
|
||||
const hours = ('' + (formModel.hours as number)).padStart(2, '0');
|
||||
const minutes = ('' + (formModel.minutes as number)).padStart(2, '0');
|
||||
if (this.item.type === 'booking') {
|
||||
console.log(formModel.date);
|
||||
this.item.bookingDate = moment(formModel.date).format('DD-MMM-YYYY') + ' ' + hours + ':' + minutes;
|
||||
item.pax = formModel.pax ?? 0;
|
||||
item.address = formModel.address;
|
||||
item.notes = formModel.notes;
|
||||
const timeStr = formModel.time ? moment(formModel.time).format('HH:mm') : '00:00';
|
||||
item.type = item.type || this.t() || 'walk_in';
|
||||
|
||||
if (item.type === 'booking') {
|
||||
item.bookingDate = moment(formModel.date).format('DD-MMM-YYYY') + ' ' + timeStr;
|
||||
} else {
|
||||
this.item.arrivalDate = (this.item.arrivalDate as string).substring(0, 12) + hours + ':' + minutes;
|
||||
const existingDate = item.arrivalDate
|
||||
? (item.arrivalDate as string).substring(0, 12)
|
||||
: moment(formModel.date).format('DD-MMM-YYYY ');
|
||||
item.arrivalDate = existingDate + timeStr;
|
||||
}
|
||||
|
||||
return this.item;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { GuestBookList } from './guest-book-list';
|
||||
import { GuestBookService } from './guest-book.service';
|
||||
|
||||
export const guestBookListResolver: ResolveFn<GuestBookList> = () => {
|
||||
return inject(GuestBookService).list(null);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { GuestBook } from '../guest-book';
|
||||
|
||||
export class GuestBookListDataSource extends DataSource<GuestBook> {
|
||||
constructor(private readonly dataObs: Observable<GuestBook[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<GuestBook[]> {
|
||||
return this.dataObs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table is being destroyed. Use this function, to clean up
|
||||
* any open connections or free any held resources that were set up during connect.
|
||||
*/
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -10,17 +10,17 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput [matDatepicker]="date" (focus)="date.open()" formControlName="date" autocomplete="off" />
|
||||
<input matInput [matDatepicker]="date" (focus)="date.open()" [formField]="form.date" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- SNo Column -->
|
||||
<ng-container matColumnDef="sno">
|
||||
<mat-header-cell *matHeaderCellDef>S. No / Total</mat-header-cell>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, effect, signal } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -9,14 +9,13 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { GuestBook } from '../guest-book';
|
||||
import { GuestBookList } from '../guest-book-list';
|
||||
import { GuestBookService } from '../guest-book.service';
|
||||
import { GuestBookListDataSource } from './guest-book-list-datasource';
|
||||
|
||||
export interface GuestBookListFormData {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-guest-book-list',
|
||||
@@ -26,53 +25,53 @@ import { GuestBookListDataSource } from './guest-book-list-datasource';
|
||||
LocalTimePipe,
|
||||
MatButtonModule,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
RouterLink,
|
||||
],
|
||||
})
|
||||
export class GuestBookListComponent implements OnInit {
|
||||
export class GuestBookListComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(GuestBookService);
|
||||
|
||||
data: BehaviorSubject<GuestBook[]> = new BehaviorSubject<GuestBook[]>([]);
|
||||
dataSource: GuestBookListDataSource = new GuestBookListDataSource(this.data);
|
||||
form: FormGroup<{
|
||||
date: FormControl<Date>;
|
||||
}>;
|
||||
formModel = signal<GuestBookListFormData>({
|
||||
date: this.initialDate(),
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['sno', 'name', 'phone', 'pax', 'date', 'action'];
|
||||
|
||||
dateSignal = computed(() => moment(this.formModel().date).format('DD-MMM-YYYY'));
|
||||
listResource = this.ser.list(this.dateSignal);
|
||||
|
||||
dataSource = computed(() => this.listResource.value()?.list ?? []);
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(new Date(), { nonNullable: true }),
|
||||
// Keep the query string in sync with the form date
|
||||
effect(() => {
|
||||
const date = this.dateSignal();
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { date },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
});
|
||||
this.listenToDateChange();
|
||||
}
|
||||
|
||||
listenToDateChange(): void {
|
||||
this.form.controls.date.valueChanges.pipe(map((x) => moment(x).format('DD-MMM-YYYY'))).subscribe((x) =>
|
||||
this.ser.list(x).subscribe((list: GuestBookList) => {
|
||||
this.data.next(list.list);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: GuestBookList };
|
||||
this.data.next(data.list.list);
|
||||
this.form.setValue({ date: moment(data.list.date, 'DD-MMM-YYYY').toDate() });
|
||||
});
|
||||
this.dataSource = new GuestBookListDataSource(this.data);
|
||||
private initialDate(): Date {
|
||||
const qp = this.route.snapshot.queryParamMap.get('date');
|
||||
if (qp) {
|
||||
const parsed = moment(qp, 'DD-MMM-YYYY', true);
|
||||
if (parsed.isValid()) return parsed.toDate();
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { guestBookDetailResolver } from './guest-book-detail.resolver';
|
||||
import { GuestBookDetailComponent } from './guest-book-detail/guest-book-detail.component';
|
||||
import { guestBookListResolver } from './guest-book-list.resolver';
|
||||
import { GuestBookListComponent } from './guest-book-list/guest-book-list.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
list: guestBookListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,9 +21,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
item: guestBookDetailResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -37,8 +29,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
item: guestBookDetailResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,10 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { GuestBook } from './guest-book';
|
||||
import { GuestBook, GuestBookType } from './guest-book';
|
||||
import { GuestBookList } from './guest-book-list';
|
||||
|
||||
const httpOptions = {
|
||||
@@ -19,19 +19,25 @@ export class GuestBookService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null, type: string): Observable<GuestBook> {
|
||||
const options = { params: new HttpParams().set('t', type) };
|
||||
const getUrl: string = id === null ? url : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<GuestBook>(getUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<GuestBook>;
|
||||
get(
|
||||
id: Signal<string | null> | string | null,
|
||||
type: Signal<GuestBookType> | GuestBookType | string = '',
|
||||
): HttpResourceRef<GuestBook | undefined> {
|
||||
return httpResource<GuestBook>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
const typeVal = typeof type === 'function' ? type() : type;
|
||||
const options = { params: new HttpParams().set('t', typeVal) };
|
||||
const getUrl: string = idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
return { url: getUrl, ...options };
|
||||
});
|
||||
}
|
||||
|
||||
list(date: string | null): Observable<GuestBookList> {
|
||||
const options = { params: new HttpParams().set('q', date === null ? '' : date) };
|
||||
return this.http
|
||||
.get<GuestBookList>(`${url}/list`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<GuestBookList>;
|
||||
list(date: Signal<string | null> | string | null): HttpResourceRef<GuestBookList | undefined> {
|
||||
return httpResource<GuestBookList>(() => {
|
||||
const dateVal = typeof date === 'function' ? date() : date;
|
||||
const options = { params: new HttpParams().set('q', dateVal === null ? '' : dateVal) };
|
||||
return { url: `${url}/list`, ...options };
|
||||
});
|
||||
}
|
||||
|
||||
save(guestBook: GuestBook): Observable<GuestBook> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type GuestBookType = 'walk_in' | 'booking';
|
||||
|
||||
export class GuestBook {
|
||||
id: string | undefined;
|
||||
serial: number;
|
||||
@@ -9,7 +11,7 @@ export class GuestBook {
|
||||
notes: string | null;
|
||||
bookingDate: string | null;
|
||||
arrivalDate: string | null;
|
||||
type: string | undefined;
|
||||
type: GuestBookType | undefined;
|
||||
lastEditDate: string | undefined;
|
||||
status?: string;
|
||||
tableId?: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Header / Footer</mat-label>
|
||||
<mat-select formControlName="id" (selectionChange)="show($event)">
|
||||
@for (s of list; track s) {
|
||||
<mat-select [formField]="form.id" (selectionChange)="show($event)">
|
||||
@for (s of list(); track s) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
@@ -14,10 +14,10 @@
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Text</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="text"></textarea>
|
||||
<textarea matInput matAutosizeMinRows="5" [formField]="form.text"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -9,92 +9,82 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { HeaderFooter } from './header-footer';
|
||||
import { HeaderFooterService } from './header-footer.service';
|
||||
|
||||
export interface HeaderFooterFormData {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-section-printer',
|
||||
templateUrl: './header-footer.component.html',
|
||||
styleUrls: ['./header-footer.component.css'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class HeaderFooterComponent implements OnInit {
|
||||
export class HeaderFooterComponent {
|
||||
@ViewChild('section', { static: true }) sectionElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private dialog = inject(MatDialog);
|
||||
private ser = inject(HeaderFooterService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
listResource = this.ser.list();
|
||||
|
||||
@ViewChild('section', { static: true }) sectionElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
id: FormControl<string>;
|
||||
text: FormControl<string>;
|
||||
}>;
|
||||
list = computed(() => this.listResource.value() ?? []);
|
||||
|
||||
list: HeaderFooter[] = [];
|
||||
id = '';
|
||||
formModel = linkedSignal({
|
||||
source: () => {
|
||||
const list = this.list();
|
||||
let currentId = this.id();
|
||||
if (!currentId && list.length > 0) {
|
||||
currentId = list[0].id;
|
||||
}
|
||||
return { list, id: currentId };
|
||||
},
|
||||
computation: ({ list, id }): HeaderFooterFormData => {
|
||||
const val = list.find((v) => v.id === id);
|
||||
return {
|
||||
id: id ?? '',
|
||||
text: val?.text ?? '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
constructor() {
|
||||
const route = this.route;
|
||||
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
id: new FormControl<string>('', { nonNullable: true }),
|
||||
text: new FormControl<string>('', { nonNullable: true }),
|
||||
});
|
||||
route.params.pipe(map((p) => p['id'])).subscribe((x) => {
|
||||
this.id = x;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: HeaderFooter[] };
|
||||
this.showItem(data.list);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(list: HeaderFooter[]) {
|
||||
this.list = list;
|
||||
this.id = this.id ?? this.list[0].id;
|
||||
const val = this.list.find((v) => v.id === this.id) as HeaderFooter;
|
||||
this.form.setValue({
|
||||
id: this.id,
|
||||
text: val.text,
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
save() {
|
||||
this.ser.save(this.getItem()).subscribe({
|
||||
next: (result: HeaderFooter[]) => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.showItem(result);
|
||||
this.listResource.value.set(result);
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getItem(): HeaderFooter {
|
||||
const formModel = this.form.value;
|
||||
const item = this.list.find((v) => v.id === this.id);
|
||||
const formModel = this.formModel();
|
||||
const item = this.list().find((v) => v.id === formModel.id);
|
||||
if (item === undefined) {
|
||||
return new HeaderFooter();
|
||||
}
|
||||
item.text = formModel.text ?? '';
|
||||
return item;
|
||||
const clonedItem = Object.assign(new HeaderFooter(), item);
|
||||
clonedItem.text = formModel.text ?? '';
|
||||
return clonedItem;
|
||||
}
|
||||
|
||||
show(val: MatSelectChange) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeaderFooterResolver } from './header-footer-resolver.service';
|
||||
|
||||
describe('HeaderFooterResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [HeaderFooterResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([HeaderFooterResolver], (service: HeaderFooterResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { HeaderFooter } from './header-footer';
|
||||
import { HeaderFooterService } from './header-footer.service';
|
||||
|
||||
export const headerFooterResolver: ResolveFn<HeaderFooter[]> = () => {
|
||||
return inject(HeaderFooterService).list();
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { HeaderFooterComponent } from './header-footer.component';
|
||||
import { headerFooterResolver } from './header-footer.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Owner',
|
||||
},
|
||||
resolve: {
|
||||
list: headerFooterResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -23,8 +19,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Owner',
|
||||
},
|
||||
resolve: {
|
||||
list: headerFooterResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
@@ -19,10 +19,8 @@ export class HeaderFooterService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<HeaderFooter[]> {
|
||||
return this.http.get<HeaderFooter[]>(url).pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<
|
||||
HeaderFooter[]
|
||||
>;
|
||||
list(): HttpResourceRef<HeaderFooter[] | undefined> {
|
||||
return httpResource<HeaderFooter[]>(() => url);
|
||||
}
|
||||
|
||||
save(item: HeaderFooter): Observable<HeaderFooter[]> {
|
||||
|
||||
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { MenuCategoryDetailComponent } from './menu-category-detail/menu-category-detail.component';
|
||||
import { menuCategoryListResolver } from './menu-category-list.resolver';
|
||||
import { MenuCategoryListComponent } from './menu-category-list/menu-category-list.component';
|
||||
import { menuCategoryResolver } from './menu-category.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Products',
|
||||
},
|
||||
resolve: {
|
||||
list: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -25,9 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Products',
|
||||
},
|
||||
resolve: {
|
||||
item: menuCategoryResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -36,8 +28,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Products',
|
||||
},
|
||||
resolve: {
|
||||
item: menuCategoryResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
+10
-6
@@ -1,19 +1,23 @@
|
||||
<h2>Menu Category</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" />
|
||||
<input matInput #nameElement [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="isActive">Is Active?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.isActive">Is Active?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button [disabled]="item.isFixture" color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button [disabled]="item.isFixture" color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button [disabled]="item().isFixture" color="primary" class="" (click)="save()">
|
||||
Save
|
||||
</button>
|
||||
@if (!!item().id) {
|
||||
<button type="button" mat-raised-button [disabled]="item().isFixture" color="warn" (click)="confirmDelete()">
|
||||
Delete
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
+34
-41
@@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -12,49 +12,40 @@ import { MenuCategory } from '../../core/menu-category';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { MenuCategoryService } from '../menu-category.service';
|
||||
|
||||
export interface MenuCategoryDetailFormData {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu-category-detail',
|
||||
templateUrl: './menu-category-detail.component.html',
|
||||
styleUrls: ['./menu-category-detail.component.css'],
|
||||
imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule],
|
||||
imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FormField],
|
||||
})
|
||||
export class MenuCategoryDetailComponent implements OnInit, AfterViewInit {
|
||||
export class MenuCategoryDetailComponent implements AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(MenuCategoryService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string>;
|
||||
isActive: FormControl<boolean>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
item: MenuCategory = new MenuCategory();
|
||||
item = computed(() => this.itemResource.value() ?? new MenuCategory());
|
||||
formModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): MenuCategoryDetailFormData => {
|
||||
return {
|
||||
name: itemVal.name,
|
||||
isActive: itemVal.isActive,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
isActive: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: MenuCategory };
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(item: MenuCategory) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
name: this.item.name,
|
||||
isActive: this.item.isActive,
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
@@ -70,20 +61,20 @@ export class MenuCategoryDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/menu-categories');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
this.ser.delete(this.item().id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/menu-categories');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -102,9 +93,11 @@ export class MenuCategoryDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): MenuCategory {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.isActive = formModel.isActive ?? true;
|
||||
return this.item;
|
||||
const formModel = this.formModel();
|
||||
return new MenuCategory({
|
||||
...this.item(),
|
||||
name: formModel.name ?? '',
|
||||
isActive: formModel.isActive ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MenuCategoryListResolver } from './menu-category-list-resolver.service';
|
||||
|
||||
describe('MenuCategoryListResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MenuCategoryListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([MenuCategoryListResolver], (service: MenuCategoryListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { MenuCategory } from '../core/menu-category';
|
||||
import { MenuCategoryService } from './menu-category.service';
|
||||
|
||||
export const menuCategoryListResolver: ResolveFn<MenuCategory[]> = () => {
|
||||
return inject(MenuCategoryService).list(false);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
|
||||
export class MenuCategoryListDatasource extends DataSource<MenuCategory> {
|
||||
private data: MenuCategory[] = [];
|
||||
|
||||
constructor(private readonly dataObs: Observable<MenuCategory[]>) {
|
||||
super();
|
||||
this.dataObs = dataObs.pipe(
|
||||
tap((x) => {
|
||||
this.data = x;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
connect(): Observable<MenuCategory[]> {
|
||||
return this.dataObs;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Menu Categories</span>
|
||||
<button mat-button (click)="updateSortOrder()">Update Order</button>
|
||||
<button type="button" mat-button (click)="updateSortOrder()">Update Order</button>
|
||||
<a mat-button [routerLink]="['/menu-categories', 'new']">
|
||||
<mat-icon>add_box</mat-icon>
|
||||
Add
|
||||
@@ -9,8 +9,8 @@
|
||||
<mat-table
|
||||
#table
|
||||
cdkDropList
|
||||
[dataSource]="dataSource"
|
||||
[cdkDropListData]="dataSource"
|
||||
[dataSource]="dataSource()"
|
||||
[cdkDropListData]="dataSource()"
|
||||
aria-label="Elements"
|
||||
(cdkDropListDropped)="dropTable($event)"
|
||||
>
|
||||
|
||||
@@ -1,63 +1,46 @@
|
||||
import { CdkDragDrop, moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
import { MenuCategoryService } from '../menu-category.service';
|
||||
import { MenuCategoryListDatasource } from './menu-category-list-datasource';
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu-category-list',
|
||||
templateUrl: './menu-category-list.component.html',
|
||||
styleUrls: ['./menu-category-list.component.css'],
|
||||
imports: [DragDropModule, MatButtonModule, MatIconModule, MatTableModule, RouterLink],
|
||||
})
|
||||
export class MenuCategoryListComponent implements OnInit {
|
||||
export class MenuCategoryListComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(MenuCategoryService);
|
||||
listResource = this.ser.list(undefined);
|
||||
|
||||
dataSource = computed(() => this.listResource.value() ?? []);
|
||||
|
||||
// @ViewChild('table', { static: true }) table?: MatTable<MenuCategory>;
|
||||
list: MenuCategory[] = [];
|
||||
data: BehaviorSubject<MenuCategory[]> = new BehaviorSubject<MenuCategory[]>(this.list);
|
||||
dataSource: MenuCategoryListDatasource = new MenuCategoryListDatasource(this.data);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'isActive', 'isFixture'];
|
||||
|
||||
constructor() {
|
||||
this.data.subscribe((data: MenuCategory[]) => {
|
||||
this.list = data;
|
||||
});
|
||||
async updateSortOrder() {
|
||||
try {
|
||||
await firstValueFrom(this.ser.updateSortOrder(this.dataSource()));
|
||||
this.snackBar.open('', 'Success');
|
||||
this.listResource.reload();
|
||||
} catch (error) {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: MenuCategory[] };
|
||||
this.data.next(data.list);
|
||||
});
|
||||
this.dataSource = new MenuCategoryListDatasource(this.data);
|
||||
}
|
||||
|
||||
updateSortOrder() {
|
||||
this.ser.updateSortOrder(this.list).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dropTable(event: CdkDragDrop<MenuCategoryListDatasource>) {
|
||||
const prevIndex = this.list.indexOf(event.item.data);
|
||||
moveItemInArray(this.list, prevIndex, event.currentIndex);
|
||||
this.data.next(this.list);
|
||||
dropTable(event: CdkDragDrop<MenuCategory[]>) {
|
||||
const arr = [...this.dataSource()];
|
||||
const prevIndex = arr.findIndex((d) => d === event.item.data);
|
||||
moveItemInArray(arr, prevIndex, event.currentIndex);
|
||||
this.listResource.set(arr);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MenuCategoryResolver } from './menu-category-resolver.service';
|
||||
|
||||
describe('MenuCategoriesResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MenuCategoryResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([MenuCategoryResolver], (service: MenuCategoryResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { MenuCategory } from '../core/menu-category';
|
||||
import { MenuCategoryService } from './menu-category.service';
|
||||
|
||||
export const menuCategoryResolver: ResolveFn<MenuCategory> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(MenuCategoryService).get(id);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -19,21 +19,21 @@ export class MenuCategoryService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<MenuCategory> {
|
||||
const getUrl: string = id === null ? url : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<MenuCategory>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<MenuCategory>;
|
||||
get(id: Signal<string | null> | string | null): HttpResourceRef<MenuCategory | undefined> {
|
||||
return httpResource<MenuCategory>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
return idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
list(shouldHaveActiveProducts: boolean | undefined): Observable<MenuCategory[]> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (shouldHaveActiveProducts) {
|
||||
options.params = options.params.set('p', 'true');
|
||||
}
|
||||
return this.http
|
||||
.get<MenuCategory[]>(`${url}/list`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<MenuCategory[]>;
|
||||
list(shouldHaveActiveProducts: boolean | undefined): HttpResourceRef<MenuCategory[] | undefined> {
|
||||
return httpResource<MenuCategory[]>(() => {
|
||||
const options = { params: new HttpParams() };
|
||||
if (shouldHaveActiveProducts) {
|
||||
options.params = options.params.set('p', 'true');
|
||||
}
|
||||
return { url: `${url}/list`, ...options };
|
||||
});
|
||||
}
|
||||
|
||||
save(menuCategory: MenuCategory): Observable<MenuCategory> {
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { merge, Observable, of as observableOf } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { MenuEngineeringReportItem } from './menu-engineering-report-item';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
|
||||
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringReportItem> {
|
||||
private filterValue = '';
|
||||
constructor(
|
||||
public data: MenuEngineeringReportItem[],
|
||||
private filter: Observable<string>,
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
this.filter = filter.pipe(
|
||||
tap((x) => {
|
||||
this.filterValue = x;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
connect(): Observable<MenuEngineeringReportItem[]> {
|
||||
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
|
||||
if (this.paginator) {
|
||||
dataMutations.push((this.paginator as MatPaginator).page);
|
||||
}
|
||||
if (this.sort) {
|
||||
dataMutations.push((this.sort as MatSort).sortChange);
|
||||
}
|
||||
return merge(observableOf(this.data), this.filter, ...dataMutations)
|
||||
.pipe(
|
||||
map(() => this.getFilteredData([...this.data])),
|
||||
map((x) => this.recalculate(x)),
|
||||
tap((x: MenuEngineeringReportItem[]) => {
|
||||
if (this.paginator) {
|
||||
this.paginator.length = x.length;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(map((x: MenuEngineeringReportItem[]) => this.getPagedData(this.getSortedData(x))));
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getFilteredData(data: MenuEngineeringReportItem[]): MenuEngineeringReportItem[] {
|
||||
return this.filterValue.split(' ').reduce(
|
||||
(p: MenuEngineeringReportItem[], c: string) =>
|
||||
p.filter((x) => {
|
||||
if (c.startsWith('n:')) {
|
||||
return x.name.toLowerCase().indexOf(c.substring(2)) !== -1;
|
||||
}
|
||||
if (c.startsWith('sc:')) {
|
||||
return x.saleCategory.toLowerCase().indexOf(c.substring(3)) !== -1;
|
||||
}
|
||||
if (c.startsWith('mc:')) {
|
||||
return x.menuCategory.toLowerCase().indexOf(c.substring(3)) !== -1;
|
||||
}
|
||||
if (c.startsWith('q:')) {
|
||||
const result = c.match(/^q:(?<sign>=|<=|>=|<|>)(?<amount>\d*)$/);
|
||||
if (result && result.groups) {
|
||||
return math_it_up(result.groups['sign'])(x.quantity, +result.groups['amount']);
|
||||
}
|
||||
}
|
||||
if (c.startsWith('s:')) {
|
||||
const result = c.match(/^s:(?<sign>=|<=|>=|<|>)(?<amount>\d*)$/);
|
||||
if (result && result.groups) {
|
||||
return math_it_up(result.groups['sign'])(x.sales, +result.groups['amount']);
|
||||
}
|
||||
}
|
||||
const itemString = `${x.name} ${x.saleCategory} ${x.menuCategory}`.toLowerCase();
|
||||
return itemString.indexOf(c) !== -1;
|
||||
}),
|
||||
Object.assign([], data),
|
||||
);
|
||||
}
|
||||
|
||||
private recalculate(data: MenuEngineeringReportItem[]): MenuEngineeringReportItem[] {
|
||||
const gr = data.reduce(function (res: Record<string, { q: number; s: number }>, value: MenuEngineeringReportItem) {
|
||||
if (!res[value['saleCategory']]) {
|
||||
res[value['saleCategory']] = { q: 0, s: 0 };
|
||||
}
|
||||
res[value['saleCategory']].q += value.quantity;
|
||||
res[value['saleCategory']].s += value.sales;
|
||||
return res;
|
||||
}, {});
|
||||
data.forEach((v) => {
|
||||
v.quantityPercent = v.quantity / gr[v.saleCategory].q;
|
||||
v.salesPercent = v.sales / gr[v.saleCategory].s;
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
private getPagedData(data: MenuEngineeringReportItem[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: MenuEngineeringReportItem[]): MenuEngineeringReportItem[] {
|
||||
if (this.sort === undefined) {
|
||||
return data;
|
||||
}
|
||||
if (!this.sort.active || this.sort.direction === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
const sort = this.sort as MatSort;
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
switch (sort.active) {
|
||||
case 'name':
|
||||
return compare(a.name, b.name, isAsc);
|
||||
case 'price':
|
||||
return compare(a.price, b.price, isAsc);
|
||||
case 'saleCategory':
|
||||
return compare(a.saleCategory, b.saleCategory, isAsc);
|
||||
case 'menuCategory':
|
||||
return compare(a.menuCategory, b.menuCategory, isAsc);
|
||||
case 'quantity':
|
||||
return compare(a.quantity, b.quantity, isAsc);
|
||||
case 'sales':
|
||||
return compare(a.sales, b.sales, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function eq(x: number, y: number) {
|
||||
return x === y;
|
||||
}
|
||||
function gt(x: number, y: number) {
|
||||
return x > y;
|
||||
}
|
||||
function lt(x: number, y: number) {
|
||||
return x < y;
|
||||
}
|
||||
function gte(x: number, y: number) {
|
||||
return x >= y;
|
||||
}
|
||||
function lte(x: number, y: number) {
|
||||
return x <= y;
|
||||
}
|
||||
|
||||
function math_it_up(sign: string) {
|
||||
if (sign == '=') {
|
||||
return eq;
|
||||
}
|
||||
if (sign == '>') {
|
||||
return gt;
|
||||
}
|
||||
if (sign == '<') {
|
||||
return lt;
|
||||
}
|
||||
if (sign == '>=') {
|
||||
return gte;
|
||||
}
|
||||
if (sign == '<=') {
|
||||
return lte;
|
||||
}
|
||||
return eq;
|
||||
}
|
||||
Loaded 100 of 350 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user