toSignal via Gemini

This commit is contained in:
tanshu committed 2026-08-18 07:55:54 +00:00
1 parent 59eb45da96
commit 993f83c786
350 files changed
+6231 -9801

No files matched your search

+17 -17
View File
@@ -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"
},
+2 -1
View File
@@ -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 },
+3
View File
@@ -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>
+34 -21
View File
@@ -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();
}));
});
-10
View File
@@ -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);
};
+9 -11
View File
@@ -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> {
-18
View File
@@ -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> {
+12
View File
@@ -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,
},
},
];
+30 -13
View File
@@ -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();
}));
});
-10
View File
@@ -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);
};
+9 -11
View File
@@ -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> {
-14
View File
@@ -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,
},
},
];
+20 -14
View File
@@ -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> {
+3 -1
View File
@@ -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,
},
},
];
@@ -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>
@@ -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