toSignal via Gemini
This commit is contained in:
1 parent
59eb45da96
commit
993f83c786
350 files changed
+6123
-9693
No files matched your search
@@ -0,0 +1,117 @@
|
||||
---
|
||||
trigger: glob
|
||||
globs: ./bookie/**/**
|
||||
---
|
||||
|
||||
# Persona
|
||||
|
||||
You are a dedicated Angular developer who thrives on leveraging the absolute latest features of the framework to build cutting-edge applications. You are currently immersed in Angular v20+, passionately adopting signals for reactive state management, embracing standalone components for streamlined architecture, and utilizing the new control flow for more intuitive template logic. Performance is paramount to you, who constantly seeks to optimize change detection and improve user experience through these modern Angular paradigms. When prompted, assume You are familiar with all the newest APIs and best practices, valuing clean, efficient, and maintainable code.
|
||||
|
||||
## Examples
|
||||
|
||||
These are modern examples of how to write an Angular 20 component with signals
|
||||
|
||||
```ts
|
||||
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: '{{tag-name}}-root',
|
||||
templateUrl: '{{tag-name}}.html',
|
||||
})
|
||||
export class {{ClassName}} {
|
||||
protected readonly isServerRunning = signal(true);
|
||||
toggleServerStatus() {
|
||||
this.isServerRunning.update(isServerRunning => !isServerRunning);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<section class="container">
|
||||
@if (isServerRunning()) {
|
||||
<span>Yes, the server is running</span>
|
||||
} @else {
|
||||
<span>No, the server is not running</span>
|
||||
}
|
||||
<button (click)="toggleServerStatus()">Toggle Server Status</button>
|
||||
</section>
|
||||
```
|
||||
|
||||
When you update a component, be sure to put the logic in the ts file, the styles in the css file and the html template in the html file.
|
||||
|
||||
## Resources
|
||||
|
||||
Here are the some links to the essentials for building Angular applications. Use these to get an understanding of how some of the core functionality works
|
||||
https://angular.dev/essentials/components
|
||||
https://angular.dev/essentials/signals
|
||||
https://angular.dev/essentials/templates
|
||||
https://angular.dev/essentials/dependency-injection
|
||||
|
||||
## Best practices & Style guide
|
||||
|
||||
Here are the best practices and the style guide information.
|
||||
|
||||
### Coding Style guide
|
||||
|
||||
Here is a link to the most recent Angular style guide https://angular.dev/style-guide
|
||||
|
||||
### TypeScript Best Practices
|
||||
|
||||
- Use strict type checking
|
||||
- Prefer type inference when the type is obvious
|
||||
- Avoid the `any` type; use `unknown` when type is uncertain
|
||||
|
||||
### Angular Best Practices
|
||||
|
||||
- Always use standalone components over `NgModules`
|
||||
- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators
|
||||
- Use signals for state management
|
||||
- Implement lazy loading for feature routes
|
||||
- Use `NgOptimizedImage` for all static images.
|
||||
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
|
||||
|
||||
### Components
|
||||
|
||||
- Keep components small and focused on a single responsibility
|
||||
- Use `input()` signal instead of decorators, learn more here https://angular.dev/guide/components/inputs
|
||||
- Use `output()` function instead of decorators, learn more here https://angular.dev/guide/components/outputs
|
||||
- Use `computed()` for derived state learn more about signals here https://angular.dev/guide/signals.
|
||||
- Prefer inline templates for small components
|
||||
- Prefer Reactive forms instead of Template-driven ones
|
||||
- Do NOT use `ngClass`, use `class` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
|
||||
- Do NOT use `ngStyle`, use `style` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
|
||||
|
||||
### State Management
|
||||
|
||||
- Use signals for local component state
|
||||
- Use `computed()` for derived state
|
||||
- Keep state transformations pure and predictable
|
||||
- Do NOT use `mutate` on signals, use `update` or `set` instead
|
||||
|
||||
### Templates
|
||||
|
||||
- Keep templates simple and avoid complex logic
|
||||
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
|
||||
- Use the async pipe to handle observables
|
||||
- Use built in pipes and import pipes when being used in a template, learn more https://angular.dev/guide/templates/pipes#
|
||||
|
||||
### Services
|
||||
|
||||
- Design services around a single responsibility
|
||||
- Use the `providedIn: 'root'` option for singleton services
|
||||
- Use the `inject()` function instead of constructor injection
|
||||
Vendored
+4
-7
@@ -14,7 +14,6 @@
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"ruff.enable": true,
|
||||
"files.eol": "\n",
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
@@ -29,7 +28,7 @@
|
||||
".idea": true,
|
||||
"**/node_modules": true,
|
||||
"**/.venv": true,
|
||||
"typings": true,
|
||||
"typings": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/__pycache__": true,
|
||||
@@ -40,12 +39,10 @@
|
||||
"**/package-lock.json": true,
|
||||
"/bookie/.angular/**": true,
|
||||
"**/.venv": true,
|
||||
"typings": true,
|
||||
"typings": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.ruff": "always",
|
||||
"source.fixAll.eslint": "always",
|
||||
},
|
||||
"python-envs.defaultEnvManager": "ms-python.python:venv",
|
||||
"python-envs.pythonProjects": []
|
||||
"source.fixAll.eslint": "always"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:lts-trixie-slim AS base
|
||||
FROM node:current-trixie-slim AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.2
|
||||
rev: v0.16.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
+19
-19
@@ -5,31 +5,31 @@ description = "Point of Sale for a restaurant"
|
||||
requires-python = ">=3.14"
|
||||
authors = [{ name = "tanshu", email = "git@tanshu.com" }]
|
||||
dependencies = [
|
||||
"aiohttp>=3.13.3",
|
||||
"alembic>=1.18.4",
|
||||
"arq>=0.27.0",
|
||||
"cryptography>=46.0.5",
|
||||
"fastapi[all]>=0.129.0",
|
||||
"gunicorn>=25.1.0",
|
||||
"aiohttp>=3.14.3",
|
||||
"alembic>=1.19.1",
|
||||
"arq>=0.28.0",
|
||||
"cryptography>=50.0.0",
|
||||
"fastapi[all]>=0.141.1",
|
||||
"gunicorn>=26.0.0",
|
||||
"itsdangerous>=2.2.0",
|
||||
"passlib[bcrypt]>=1.7.4",
|
||||
"psycopg[binary,pool]>=3.3.3",
|
||||
"pydantic>=2.12.5",
|
||||
"pydantic-settings>=2.13.1",
|
||||
"pyjwt>=2.11.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"python-multipart>=0.0.22",
|
||||
"sqlalchemy>=2.0.46",
|
||||
"uvicorn[standard]>=0.41.0",
|
||||
"psycopg[binary,pool]>=3.3.4",
|
||||
"pydantic>=2.13.4",
|
||||
"pydantic-settings>=2.15.0",
|
||||
"pyjwt>=2.13.0",
|
||||
"python-dotenv>=1.2.2",
|
||||
"python-multipart>=0.0.32",
|
||||
"sqlalchemy>=2.0.51",
|
||||
"uvicorn[standard]>=0.52.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"bandit>=1.9.3",
|
||||
"mypy>=1.19.1",
|
||||
"pre-commit>=4.5.1",
|
||||
"ruff>=0.15.2",
|
||||
"safety>=3.7.0",
|
||||
"bandit>=1.9.4",
|
||||
"mypy>=2.3.0",
|
||||
"pre-commit>=4.6.1",
|
||||
"ruff>=0.16.2",
|
||||
"safety>=3.8.1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
Generated
+407
-363
File diff suppressed because it is too large.
Load diff
+17
-17
@@ -11,36 +11,36 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^21.1.1",
|
||||
"@angular/common": "^21.1.1",
|
||||
"@angular/compiler": "^21.1.1",
|
||||
"@angular/core": "^21.1.1",
|
||||
"@angular/forms": "^21.1.1",
|
||||
"@angular/material": "^21.1.1",
|
||||
"@angular/material-moment-adapter": "^21.1.1",
|
||||
"@angular/platform-browser": "^21.1.1",
|
||||
"@angular/platform-browser-dynamic": "^21.1.1",
|
||||
"@angular/router": "^21.1.1",
|
||||
"@angular/cdk": "^22.1.1",
|
||||
"@angular/common": "^22.1.1",
|
||||
"@angular/compiler": "^22.1.1",
|
||||
"@angular/core": "^22.1.1",
|
||||
"@angular/forms": "^22.1.1",
|
||||
"@angular/material": "^22.1.1",
|
||||
"@angular/material-moment-adapter": "^22.1.1",
|
||||
"@angular/platform-browser": "^22.1.1",
|
||||
"@angular/router": "^22.1.1",
|
||||
"mathjs": "^15.1.0",
|
||||
"moment": "^2.30.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^21.1.1",
|
||||
"@angular/cli": "^21.1.1",
|
||||
"@angular/compiler-cli": "^21.1.1",
|
||||
"angular-eslint": "^21.1.0",
|
||||
"eslint": "^9.28.0",
|
||||
"@angular/build": "^22.1.3",
|
||||
"@angular/cli": "^22.1.3",
|
||||
"@angular/compiler-cli": "^22.1.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"angular-eslint": "^22.1.0",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-perfectionist": "^5.4.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lint-staged": "^17.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vitest": "^4.0.8"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { provideRouter, withRouterConfig } from '@angular/router';
|
||||
import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
|
||||
|
||||
import { dateFormat } from './app.environment';
|
||||
import { routes } from './app.routes';
|
||||
@@ -55,6 +55,7 @@ export const appConfig: ApplicationConfig = {
|
||||
withRouterConfig({
|
||||
onSameUrlNavigation: 'reload',
|
||||
}),
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: dateFormat },
|
||||
|
||||
@@ -7,9 +7,12 @@ export const environment = {
|
||||
export const dateFormat = {
|
||||
parse: {
|
||||
dateInput: 'DD-MMM-YYYY',
|
||||
timeInput: 'hh:mm A',
|
||||
},
|
||||
display: {
|
||||
dateInput: 'DD-MMM-YYYY',
|
||||
timeInput: 'hh:mm A',
|
||||
timeOptionLabel: 'hh:mm A',
|
||||
monthYearLabel: 'MMM YYYY',
|
||||
dateA11yLabel: 'DD-MMM-YYYY',
|
||||
monthYearA11yLabel: 'MMM YYYY',
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<h2>Login</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput #nameElement formControlName="username" />
|
||||
<input matInput #nameElement [formField]="form.username" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput formControlName="password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
|
||||
<input matInput [formField]="form.password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
|
||||
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@if (unregisteredDevice) {
|
||||
@if (unregisteredDevice()) {
|
||||
<mat-divider></mat-divider>
|
||||
<h2>Sorry, device {{ deviceName }} is not enabled.</h2>
|
||||
<h2>Sorry, device {{ deviceName() }} is not enabled.</h2>
|
||||
}
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button (click)="login()" color="primary">Login</button>
|
||||
<button type="button" mat-raised-button (click)="login()" color="primary">Login</button>
|
||||
</div>
|
||||
@@ -1,5 +1,14 @@
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { form, FormField, required } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -11,13 +20,19 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CookieService } from '../../shared/cookie.service';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
export interface LoginFormData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css'],
|
||||
imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, ReactiveFormsModule],
|
||||
imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, FormField],
|
||||
})
|
||||
export class LoginComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
@@ -25,28 +40,26 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
private cs = inject(CookieService);
|
||||
private cd = inject(ChangeDetectorRef);
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
username: FormControl<string>;
|
||||
password: FormControl<string>;
|
||||
}>;
|
||||
formModel = signal<LoginFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
form = form(this.formModel, (schemaPath) => {
|
||||
required(schemaPath.username);
|
||||
required(schemaPath.password);
|
||||
});
|
||||
|
||||
hide: boolean;
|
||||
unregisteredDevice: boolean;
|
||||
deviceName: string;
|
||||
unregisteredDevice = signal<boolean | undefined>(undefined);
|
||||
deviceName = signal<string | undefined>(undefined);
|
||||
private returnUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.hide = true;
|
||||
this.unregisteredDevice = false;
|
||||
this.deviceName = '';
|
||||
this.unregisteredDevice.set(false);
|
||||
this.deviceName.set('');
|
||||
this.returnUrl = '';
|
||||
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
username: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
password: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -62,7 +75,7 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
login(): void {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
const username = formModel.username ?? '';
|
||||
const password = formModel.password ?? '';
|
||||
this.auth.login(username.trim(), password.trim()).subscribe({
|
||||
@@ -71,8 +84,8 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
},
|
||||
error: (error) => {
|
||||
if (error.status === 401 && error.error.detail === 'Device is not registered') {
|
||||
this.unregisteredDevice = true;
|
||||
this.deviceName = this.cs.getCookie('device');
|
||||
this.unregisteredDevice.set(true);
|
||||
this.deviceName.set(this.cs.getCookie('device'));
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
this.snackBar.open(error.error.detail, 'Error');
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { BeerSaleReportItem } from './beer-sale-report-item';
|
||||
|
||||
export class BeerSaleReportDataSource extends DataSource<BeerSaleReportItem> {
|
||||
constructor(public data: BeerSaleReportItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BeerSaleReportItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
<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">
|
||||
@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>
|
||||
@@ -13,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>
|
||||
@@ -25,7 +28,7 @@
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
@@ -33,24 +36,36 @@
|
||||
</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-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-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"
|
||||
[disabled]="form().invalid()"
|
||||
(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>
|
||||
<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">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef>Time</mat-header-cell>
|
||||
@@ -58,13 +73,14 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
@for (col of info.headers; track col) {
|
||||
@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-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 }),
|
||||
displayedColumns = computed(() => {
|
||||
const headers = this.info()?.headers ?? [];
|
||||
return ['date'].concat(headers);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
const headers = currentInfo.headers.reduce(
|
||||
(a: Record<string, string>, c: string) => {
|
||||
a[c] = c;
|
||||
return a;
|
||||
}, init);
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.info.data)], {
|
||||
},
|
||||
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);
|
||||
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 (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
if (saleCategory != null) {
|
||||
options.params = options.params.set('sc', saleCategory);
|
||||
if (sc !== null) {
|
||||
params = params.set('sc', sc);
|
||||
}
|
||||
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>;
|
||||
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);
|
||||
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 (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return this.http
|
||||
.get<BillSettlementReport>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable<BillSettlementReport>;
|
||||
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>;
|
||||
}>;
|
||||
|
||||
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 }),
|
||||
formModel = signal<BundleDetailDialogFormData>({
|
||||
product: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: 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);
|
||||
}),
|
||||
);
|
||||
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() {
|
||||
// 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,127 +68,32 @@ 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$);
|
||||
|
||||
item: Bundle = new Bundle();
|
||||
|
||||
itemProduct: ProductQuery | null = null;
|
||||
itemProducts: Observable<ProductQuery[]>;
|
||||
|
||||
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) => {
|
||||
if (typeof x === 'string') {
|
||||
this.itemProduct = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
displayFn(product?: ProductQuery | string): string {
|
||||
return !product ? '' : typeof product === 'string' ? product : product.name;
|
||||
}
|
||||
|
||||
bundleSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const product = event.option.value as ProductQuery;
|
||||
this.itemProduct = product;
|
||||
this.form.controls.addRow.controls.salePrice.setValue(product.price ?? 0);
|
||||
}
|
||||
|
||||
private recalcBundleSalePrice(): void {
|
||||
const total = (this.item.items ?? []).reduce((sum, x) => {
|
||||
const qty = Number(x.quantity ?? 0);
|
||||
const price = Number(x.salePrice ?? 0);
|
||||
if (Number.isNaN(qty) || Number.isNaN(price)) return sum;
|
||||
return sum + qty * price;
|
||||
}, 0);
|
||||
|
||||
// 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({
|
||||
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),
|
||||
@@ -176,13 +108,105 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: '',
|
||||
units: '',
|
||||
salePrice: 0,
|
||||
menuCategory: '',
|
||||
saleCategory: '',
|
||||
hasHappyHour: false,
|
||||
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 ?? []);
|
||||
form = form(this.formModel);
|
||||
|
||||
menuCategories = signal<MenuCategory[]>([]);
|
||||
saleCategories = signal<SaleCategory[]>([]);
|
||||
items = signal<BundleItem[]>([]);
|
||||
dataSource = computed(() => this.items());
|
||||
|
||||
item: Bundle = new Bundle();
|
||||
|
||||
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() {
|
||||
effect(() => {
|
||||
const x = this.formModel().addRow?.itemId;
|
||||
if (typeof x === 'string') {
|
||||
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;
|
||||
|
||||
this.itemProduct.set(null);
|
||||
|
||||
this.items.set(this.item.items ?? []);
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayFn(product?: ProductQuery | string): string {
|
||||
return !product ? '' : typeof product === 'string' ? product : product.name;
|
||||
}
|
||||
|
||||
bundleSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const product = event.option.value as ProductQuery;
|
||||
this.itemProduct.set(product);
|
||||
this.formModel.update((f) => ({
|
||||
...f,
|
||||
addRow: {
|
||||
...f.addRow,
|
||||
salePrice: product.price ?? 0,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private recalcBundleSalePrice(): void {
|
||||
const total = (this.item.items ?? []).reduce((sum, x) => {
|
||||
const qty = Number(x.quantity ?? 0);
|
||||
const price = Number(x.salePrice ?? 0);
|
||||
if (Number.isNaN(qty) || Number.isNaN(price)) return sum;
|
||||
return sum + qty * price;
|
||||
}, 0);
|
||||
|
||||
// keep model + form in sync
|
||||
this.item.salePrice = Number(total.toFixed(2));
|
||||
|
||||
this.formModel.update((f) => ({ ...f, salePrice: this.item.salePrice }));
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
@@ -193,17 +217,25 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
resetAddRow() {
|
||||
this.form.controls.addRow.reset();
|
||||
this.itemProduct = null;
|
||||
this.formModel.update((f) => ({
|
||||
...f,
|
||||
addRow: {
|
||||
itemId: '',
|
||||
quantity: 1,
|
||||
salePrice: 0,
|
||||
printInBill: true,
|
||||
},
|
||||
}));
|
||||
this.itemProduct.set(null);
|
||||
}
|
||||
|
||||
addRow() {
|
||||
const v = this.form.value.addRow;
|
||||
const v = this.formModel().addRow;
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.itemProduct?.id) {
|
||||
if (!this.itemProduct()?.id) {
|
||||
this.snackBar.open('Please select a product', 'Error');
|
||||
return;
|
||||
}
|
||||
@@ -223,15 +255,15 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
// name is expected from backend as "name (units)" on GET;
|
||||
// for new rows we keep it blank (or you can set placeholder)
|
||||
const bi = new BundleItem({
|
||||
itemId: this.itemProduct?.id,
|
||||
name: this.itemProduct?.name ?? '',
|
||||
itemId: this.itemProduct()?.id,
|
||||
name: this.itemProduct()?.name ?? '',
|
||||
quantity,
|
||||
salePrice,
|
||||
printInBill: v.printInBill || false,
|
||||
});
|
||||
|
||||
this.item.items.push(bi);
|
||||
this.items$.next(this.item.items);
|
||||
this.items.set(this.item.items);
|
||||
this.resetAddRow();
|
||||
|
||||
this.recalcBundleSalePrice();
|
||||
@@ -250,7 +282,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
return;
|
||||
}
|
||||
Object.assign(row, result as BundleItem);
|
||||
this.items$.next(this.item.items);
|
||||
this.items.set(this.item.items);
|
||||
this.resetAddRow();
|
||||
this.recalcBundleSalePrice();
|
||||
});
|
||||
@@ -258,7 +290,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
|
||||
deleteRow(row: BundleItem) {
|
||||
this.item.items.splice(this.item.items.indexOf(row), 1);
|
||||
this.items$.next(this.item.items);
|
||||
this.items.set(this.item.items);
|
||||
this.recalcBundleSalePrice();
|
||||
}
|
||||
|
||||
@@ -272,8 +304,8 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/bundles');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -284,8 +316,8 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/bundles');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -304,7 +336,7 @@ export class BundleDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Bundle {
|
||||
const v = this.form.value;
|
||||
const v = this.formModel();
|
||||
|
||||
this.item.name = v.name ?? '';
|
||||
this.item.units = v.units ?? '';
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleListResolver } from './bundle-list-resolver.service';
|
||||
|
||||
describe('BundleListResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleListResolver], (service: BundleListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Bundle } from './bundle';
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
export const bundleListResolver: ResolveFn<Bundle[]> = () => {
|
||||
return inject(BundleService).list();
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { Bundle } from '../bundle';
|
||||
|
||||
export class BundleListDataSource extends DataSource<Bundle> {
|
||||
public data: Bundle[];
|
||||
public filteredData: Bundle[];
|
||||
public search: string;
|
||||
public menuCategory: string;
|
||||
|
||||
constructor(
|
||||
private readonly searchFilter: Observable<string>,
|
||||
private readonly menuCategoryFilter: Observable<string>,
|
||||
private readonly dataObs: Observable<Bundle[]>,
|
||||
) {
|
||||
super();
|
||||
this.data = [];
|
||||
this.filteredData = [];
|
||||
this.search = '';
|
||||
this.menuCategory = '';
|
||||
}
|
||||
|
||||
connect(): Observable<Bundle[]> {
|
||||
const dataMutations = [
|
||||
this.dataObs.pipe(
|
||||
tap((x) => {
|
||||
this.data = x;
|
||||
}),
|
||||
),
|
||||
this.searchFilter.pipe(
|
||||
tap((x) => {
|
||||
this.search = x;
|
||||
}),
|
||||
),
|
||||
this.menuCategoryFilter.pipe(
|
||||
tap((x) => {
|
||||
this.menuCategory = x;
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
return merge(...dataMutations).pipe(
|
||||
map(() => this.getFilteredData(this.data, this.search, this.menuCategory)),
|
||||
tap((x) => {
|
||||
this.filteredData = x;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getFilteredData(data: Bundle[], search: string, menuCategory: string): Bundle[] {
|
||||
const tokens = (search ?? '').toLowerCase().split(/\s+/).filter(Boolean);
|
||||
|
||||
return data.filter((b: Bundle) => {
|
||||
const mcName = b.menuCategory?.name ?? '';
|
||||
const hay = `${b.name ?? ''} ${b.units ?? ''} ${mcName}`.toLowerCase();
|
||||
|
||||
const matchesSearch = tokens.length === 0 || tokens.every((t) => hay.includes(t));
|
||||
const matchesMenuCategory = menuCategory === '' || (b.menuCategory?.id ?? '') === menuCategory;
|
||||
|
||||
return matchesSearch && matchesMenuCategory;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Bundles</span>
|
||||
<button mat-icon-button (click)="exportCsv()" [disabled]="!(data | async)?.length">
|
||||
<button type="button" mat-icon-button (click)="exportCsv()" [disabled]="!dataSource().length">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<a mat-button [routerLink]="['/bundles', 'new']">
|
||||
@@ -9,18 +9,18 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input type="text" matInput formControlName="filter" autocomplete="off" />
|
||||
<input type="text" matInput [formField]="form.filter" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select formControlName="menuCategory" (selectionChange)="filterOn($event.value)">
|
||||
<mat-select [formField]="form.menuCategory" (selectionChange)="filterOn($event.value)">
|
||||
<mat-option>-- All Bundles --</mat-option>
|
||||
@for (mc of menuCategories; track mc) {
|
||||
@for (mc of menuCategoriesResource.value(); track mc) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Bundles">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Bundles">
|
||||
<!-- Name -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, inject, computed, signal, debounced } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -8,21 +8,23 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { MenuCategory } from '../../core/menu-category';
|
||||
import { MenuCategoryService } from '../../menu-category/menu-category.service';
|
||||
import { ToCsvService } from '../../shared/to-csv.service';
|
||||
import { Bundle } from '../bundle';
|
||||
import { BundleListDataSource } from './bundle-list-datasource';
|
||||
import { BundleService } from '../bundle.service';
|
||||
|
||||
export interface BundleListFormData {
|
||||
menuCategory: string | null;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-bundle-list',
|
||||
templateUrl: './bundle-list.component.html',
|
||||
styleUrls: ['./bundle-list.component.css'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
@@ -31,58 +33,44 @@ import { BundleListDataSource } from './bundle-list-datasource';
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class BundleListComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class BundleListComponent {
|
||||
private menuCategoryService = inject(MenuCategoryService);
|
||||
private bundleService = inject(BundleService);
|
||||
private toCsv = inject(ToCsvService);
|
||||
|
||||
searchFilter = new BehaviorSubject<string>('');
|
||||
menuCategoryFilter = new BehaviorSubject<string>('');
|
||||
data: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||
dataSource: BundleListDataSource = new BundleListDataSource(this.searchFilter, this.menuCategoryFilter, this.data);
|
||||
menuCategoriesResource = this.menuCategoryService.list(undefined);
|
||||
listResource = this.bundleService.list();
|
||||
|
||||
form: FormGroup<{
|
||||
menuCategory: FormControl<string | null>;
|
||||
filter: FormControl<string>;
|
||||
}>;
|
||||
menuCategoryFilter = signal<string>('');
|
||||
|
||||
formModel = signal<BundleListFormData>({
|
||||
menuCategory: '',
|
||||
filter: '',
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
filterSignal = computed(() => this.formModel().filter);
|
||||
debouncedFilter = debounced(this.filterSignal, 150);
|
||||
|
||||
dataSource = computed(() => {
|
||||
let items = this.listResource.value() ?? [];
|
||||
const search = this.debouncedFilter.value()?.toLowerCase() ?? '';
|
||||
const cat = this.menuCategoryFilter();
|
||||
|
||||
if (search) items = items.filter((x: Bundle) => x.name.toLowerCase().includes(search));
|
||||
if (cat) items = items.filter((x: Bundle) => x.menuCategory?.id === cat);
|
||||
return items;
|
||||
});
|
||||
|
||||
list: Bundle[] = [];
|
||||
menuCategories: MenuCategory[] = [];
|
||||
displayedColumns: string[] = ['name', 'price', 'menuCategory', 'saleCategory', 'info', 'items'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
menuCategory: new FormControl<string | null>(''),
|
||||
filter: new FormControl<string>('', { nonNullable: true }),
|
||||
});
|
||||
|
||||
this.data.subscribe((data: Bundle[]) => {
|
||||
this.list = data;
|
||||
});
|
||||
|
||||
this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged()).subscribe((value) => {
|
||||
this.searchFilter.next(value ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
filterOn(val: string) {
|
||||
this.menuCategoryFilter.next(val);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource = new BundleListDataSource(this.searchFilter, this.menuCategoryFilter, this.data);
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Bundle[]; menuCategories: MenuCategory[] };
|
||||
this.loadData(data.list, data.menuCategories);
|
||||
});
|
||||
}
|
||||
|
||||
loadData(list: Bundle[], menuCategories: MenuCategory[]) {
|
||||
this.menuCategories = menuCategories;
|
||||
this.data.next(list);
|
||||
this.menuCategoryFilter.set(val);
|
||||
}
|
||||
|
||||
exportCsv() {
|
||||
@@ -96,7 +84,7 @@ export class BundleListComponent implements OnInit {
|
||||
};
|
||||
|
||||
// Map items to count for csv (simple)
|
||||
const csvList = this.dataSource.filteredData.map((b) => ({
|
||||
const csvList = this.dataSource().map((b: Bundle) => ({
|
||||
...b,
|
||||
menuCategory: b.menuCategory?.name ?? '',
|
||||
items: (b.items ?? [])
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BundleResolver } from './bundle-resolver.service';
|
||||
|
||||
describe('BundleResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [BundleResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([BundleResolver], (service: BundleResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
|
||||
|
||||
import { Bundle } from './bundle';
|
||||
import { BundleService } from './bundle.service';
|
||||
|
||||
export const bundleResolver: ResolveFn<Bundle> = (route: ActivatedRouteSnapshot) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(BundleService).get(id);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -18,17 +18,15 @@ export class BundleService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<Bundle[]> {
|
||||
return this.http
|
||||
.get<Bundle[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Bundle[]>;
|
||||
list(): HttpResourceRef<Bundle[] | undefined> {
|
||||
return httpResource<Bundle[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
get(id: string | null): Observable<Bundle> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Bundle>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Bundle>;
|
||||
get(id: Signal<string | null>): HttpResourceRef<Bundle | undefined> {
|
||||
return httpResource<Bundle>(() => {
|
||||
const idVal = id();
|
||||
return idVal === null ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
save(bundle: Bundle): Observable<void> {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { menuCategoryListResolver } from '../menu-category/menu-category-list.resolver';
|
||||
import { saleCategoryListResolver } from '../sale-category/sale-category-list.resolver';
|
||||
import { BundleDetailComponent } from './bundle-detail/bundle-detail.component';
|
||||
import { bundleListResolver } from './bundle-list.resolver';
|
||||
import { BundleListComponent } from './bundle-list/bundle-list.component';
|
||||
import { bundleResolver } from './bundle.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,31 +10,17 @@ export const routes: Routes = [
|
||||
component: BundleListComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' }, // change if you have a separate permission
|
||||
resolve: {
|
||||
list: bundleListResolver,
|
||||
menuCategories: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: BundleDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' },
|
||||
resolve: {
|
||||
item: bundleResolver, // returns new Bundle() because id === null (no param here)
|
||||
menuCategories: menuCategoryListResolver,
|
||||
saleCategories: saleCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: BundleDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { permission: 'Products' },
|
||||
resolve: {
|
||||
item: bundleResolver,
|
||||
menuCategories: menuCategoryListResolver,
|
||||
saleCategories: saleCategoryListResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActiveCashiersResolver } from './active-cashiers-resolver.service';
|
||||
|
||||
describe('ActiveCashiersResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ActiveCashiersResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ActiveCashiersResolver], (service: ActiveCashiersResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { User } from '../core/user';
|
||||
import { CashierReportService } from './cashier-report.service';
|
||||
|
||||
export const activeCashiersResolver: ResolveFn<User[]> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(CashierReportService).activeCashiers(startDate, finishDate);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { CashierReportDisplayItem } from './cashier-report-display-item';
|
||||
|
||||
export class CashierReportDataSource extends DataSource<CashierReportDisplayItem> {
|
||||
constructor(public data: CashierReportDisplayItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<CashierReportDisplayItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Cashier Report</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button [disabled]="!info.cashier.id" (click)="exportCsv()">
|
||||
<button type="button" mat-icon-button [disabled]="!info().cashier.id" (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [disabled]="!info.cashier.id" (click)="print()">
|
||||
<button type="button" mat-icon-button [disabled]="!info().cashier.id" (click)="print()">
|
||||
<mat-icon>print</mat-icon>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Start Date</mat-label>
|
||||
@@ -17,7 +17,7 @@
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
formControlName="startDate"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
@@ -29,7 +29,7 @@
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
@@ -39,19 +39,19 @@
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-4-5">
|
||||
<mat-label>Cashier</mat-label>
|
||||
<mat-select formControlName="cashier">
|
||||
<mat-select [formField]="form.cashier">
|
||||
<mat-option>-- Cashier --</mat-option>
|
||||
@for (ac of activeCashiers; track ac) {
|
||||
@for (ac of cashiers(); track ac) {
|
||||
<mat-option [value]="ac.id">
|
||||
{{ ac.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
<button type="button" mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
@@ -16,9 +16,14 @@ import moment from 'moment';
|
||||
import { User } from '../core/user';
|
||||
import { ToCsvService } from '../shared/to-csv.service';
|
||||
import { CashierReport } from './cashier-report';
|
||||
import { CashierReportDataSource } from './cashier-report-datasource';
|
||||
import { CashierReportService } from './cashier-report.service';
|
||||
|
||||
export interface CashierReportFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
cashier: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-cashier-report',
|
||||
templateUrl: './cashier-report.component.html',
|
||||
@@ -26,7 +31,6 @@ import { CashierReportService } from './cashier-report.service';
|
||||
imports: [
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
@@ -34,51 +38,49 @@ import { CashierReportService } from './cashier-report.service';
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class CashierReportComponent implements OnInit {
|
||||
export class CashierReportComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private toCsv = inject(ToCsvService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(CashierReportService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
activeCashiers: User[] = [];
|
||||
info: CashierReport = new CashierReport();
|
||||
dataSource: CashierReportDataSource = new CashierReportDataSource(this.info.amounts);
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
cashier: FormControl<string | null>;
|
||||
}>;
|
||||
infoResource = this.ser.list(this.id, this.startDate, this.finishDate);
|
||||
cashiersResource = this.ser.activeCashiers(this.startDate, this.finishDate);
|
||||
|
||||
cashiers = computed(() => (this.cashiersResource.value() ?? []) as User[]);
|
||||
|
||||
info = computed(() => this.infoResource.value() ?? new CashierReport());
|
||||
dataSource = computed(() => this.info().amounts);
|
||||
formModel = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (infoVal): CashierReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
cashier: this.info().cashier.id ?? '',
|
||||
startDate: moment(this.info().startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info().finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
startDate: new Date(),
|
||||
finishDate: new Date(),
|
||||
cashier: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'amount'];
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
cashier: new FormControl<string | null>(null),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { cashiers: User[]; info: CashierReport };
|
||||
this.activeCashiers = data.cashiers;
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
cashier: this.info.cashier.id ?? '',
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
});
|
||||
this.dataSource = new CashierReportDataSource(this.info.amounts);
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
const url = ['cashier-report'];
|
||||
@@ -94,22 +96,22 @@ export class CashierReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
print() {
|
||||
this.ser.print(this.info.cashier.id as string, this.info.startDate, this.info.finishDate).subscribe({
|
||||
this.ser.print(this.info().cashier.id as string, this.info().startDate, this.info().finishDate).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Successfully Printed', '');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getInfo(): CashierReport {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new CashierReport({
|
||||
cashier: new User({ id: formModel.cashier ?? undefined }),
|
||||
cashiers: this.info.cashiers,
|
||||
cashiers: this.info().cashiers,
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
|
||||
});
|
||||
@@ -120,7 +122,7 @@ export class CashierReportComponent implements OnInit {
|
||||
Name: 'name',
|
||||
Amount: 'amount',
|
||||
};
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource())], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CashierReportResolver } from './cashier-report-resolver.service';
|
||||
|
||||
describe('CashierReportResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [CashierReportResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CashierReportResolver], (service: CashierReportResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { CashierReport } from './cashier-report';
|
||||
import { CashierReportService } from './cashier-report.service';
|
||||
|
||||
export const cashierReportResolver: ResolveFn<CashierReport> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(CashierReportService).list(id, startDate, finishDate);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { activeCashiersResolver } from './active-cashiers.resolver';
|
||||
import { CashierReportComponent } from './cashier-report.component';
|
||||
import { cashierReportResolver } from './cashier-report.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cashier Report',
|
||||
},
|
||||
resolve: {
|
||||
info: cashierReportResolver,
|
||||
cashiers: activeCashiersResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cashier Report',
|
||||
},
|
||||
resolve: {
|
||||
info: cashierReportResolver,
|
||||
cashiers: activeCashiersResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -17,32 +17,43 @@ export class CashierReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(id: string | null, startDate: string | null, finishDate: string | null): Observable<CashierReport> {
|
||||
const listUrl = id === null ? url : `${url}/${id}`;
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
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 (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return this.http
|
||||
.get<CashierReport>(listUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<CashierReport>;
|
||||
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);
|
||||
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 (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return this.http
|
||||
.get<User[]>(`${url}/active`, options)
|
||||
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'activeCashiers'))) as Observable<User[]>;
|
||||
return { url: `${url}/active`, params };
|
||||
});
|
||||
}
|
||||
|
||||
print(id: string, startDate: string | null, finishDate: string | null): Observable<boolean> {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export class UserRole {
|
||||
id: string | undefined;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
|
||||
public constructor(init?: Partial<UserRole>) {
|
||||
this.id = undefined;
|
||||
this.name = '';
|
||||
this.enabled = false;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,41 @@
|
||||
<h2>Customer</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" />
|
||||
<input matInput #nameElement [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" />
|
||||
<input matInput [formField]="form.phone" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Address</mat-label>
|
||||
<textarea matInput formControlName="address"> </textarea>
|
||||
<textarea matInput [formField]="form.address"> </textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="printInBill">Print in Bill?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.printInBill">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div formArrayName="discounts" class="discount-grid">
|
||||
@for (r of item.discounts; track r; let i = $index) {
|
||||
<mat-form-field [formGroupName]="i">
|
||||
<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 formControlName="discount" />
|
||||
<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;
|
||||
|
||||
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> }>>([]),
|
||||
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 })),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: Customer };
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
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;
|
||||
}
|
||||
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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
return this.item;
|
||||
}
|
||||
}
|
||||
@@ -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: () =>
|
||||
// Sync query string whenever params change
|
||||
effect(() => {
|
||||
const p = this.queryParams();
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { f: this.sort?.active ?? 'name', d: this.sort?.direction },
|
||||
queryParams: { q: p.q, p: p.p, s: p.s, f: p.f, d: p.d },
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
replaceUrl: true,
|
||||
});
|
||||
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',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.sort.sortChange.subscribe(() => {
|
||||
this.sortField.set(this.sort.active);
|
||||
this.sortDirection.set(this.sort.direction);
|
||||
this.pageIndex.set(0);
|
||||
});
|
||||
|
||||
this.paginator.page.subscribe(() => {
|
||||
this.pageSize.set(this.paginator.pageSize);
|
||||
this.pageIndex.set(this.paginator.pageIndex);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.filterElement.nativeElement.focus();
|
||||
}, 0);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CustomerResolver } from './customer-resolver.service';
|
||||
|
||||
describe('CustomerResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [CustomerResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CustomerResolver], (service: CustomerResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Customer } from '../core/customer';
|
||||
import { CustomerService } from './customer.service';
|
||||
|
||||
export const customerResolver: ResolveFn<Customer> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(CustomerService).get(id);
|
||||
};
|
||||
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { CustomerDetailComponent } from './customer-detail/customer-detail.component';
|
||||
import { customerListResolver } from './customer-list.resolver';
|
||||
import { CustomerListComponent } from './customer-list/customer-list.component';
|
||||
import { customerResolver } from './customer.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
data: customerListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,16 +21,10 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
item: customerResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: CustomerDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
resolve: {
|
||||
item: customerResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -20,11 +20,11 @@ export class CustomerService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<Customer> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Customer>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Customer>;
|
||||
get(id: Signal<string | null> | string | null): HttpResourceRef<Customer | undefined> {
|
||||
return httpResource<Customer>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
return idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
list(q: string | null, page = 0, size = 50, field = 'name', direction = 'asc'): Observable<PagedResult<Customer>> {
|
||||
@@ -35,12 +35,29 @@ export class CustomerService {
|
||||
f: field,
|
||||
d: direction,
|
||||
};
|
||||
console.log('CustomerService.list', params);
|
||||
return this.http
|
||||
.get<PagedResult<Customer>>(`${url}/list`, { params })
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<PagedResult<Customer>>;
|
||||
}
|
||||
|
||||
query(
|
||||
params: Signal<{ q: string; p: number; s: number; f: string; d: string }>,
|
||||
): HttpResourceRef<PagedResult<Customer> | undefined> {
|
||||
return httpResource<PagedResult<Customer>>(() => {
|
||||
const p = params();
|
||||
return {
|
||||
url: `${url}/list`,
|
||||
params: {
|
||||
...(p.q ? { q: p.q } : {}),
|
||||
p: p.p,
|
||||
s: p.s,
|
||||
f: p.f,
|
||||
d: p.d,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
save(customer: Customer): Observable<Customer> {
|
||||
return this.http
|
||||
.post<Customer>(`${url}`, customer, httpOptions)
|
||||
@@ -66,10 +83,10 @@ export class CustomerService {
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Customer>;
|
||||
}
|
||||
|
||||
autocomplete(query: string): Observable<Customer[]> {
|
||||
const options = { params: new HttpParams().set('q', query) };
|
||||
return this.http
|
||||
.get<Customer[]>(`${url}/query`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Customer[]>;
|
||||
autocomplete(query: Signal<string | null> | string | null): HttpResourceRef<Customer[] | undefined> {
|
||||
return httpResource<Customer[]>(() => {
|
||||
const qVal = typeof query === 'function' ? query() : query;
|
||||
return `${url}/query?q=${qVal ?? ''}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<h2>Device</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" (keyup.enter)="save()" />
|
||||
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Section</mat-label>
|
||||
<mat-select formControlName="section">
|
||||
@for (s of sections; track s) {
|
||||
<mat-select [formField]="form.section">
|
||||
@for (s of sections(); track s) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
@@ -19,16 +19,16 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="enabled" class="flex-auto">Enabled?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.enabled" class="flex-auto">Enabled?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container space-between">
|
||||
<button mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item().id) {
|
||||
<button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
<div class="spacer"></div>
|
||||
<span
|
||||
>Created on <strong>{{ item.creationDate | localTime }}</strong></span
|
||||
>Created on <strong>{{ item().creationDate | localTime }}</strong></span
|
||||
>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
@@ -12,10 +12,17 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Device } from '../../core/device';
|
||||
import { Section } from '../../core/section';
|
||||
import { SectionService } from '../../sections/section.service';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { DeviceService } from '../device.service';
|
||||
|
||||
export interface DeviceDetailFormData {
|
||||
name: string;
|
||||
section: string | null;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-device-detail',
|
||||
templateUrl: './device-detail.component.html',
|
||||
@@ -23,58 +30,43 @@ import { DeviceService } from '../device.service';
|
||||
imports: [
|
||||
LocalTimePipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class DeviceDetailComponent implements OnInit, AfterViewInit {
|
||||
export class DeviceDetailComponent implements AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
private sectionService = inject(SectionService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(DeviceService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string>;
|
||||
section: FormControl<string | null>;
|
||||
enabled: FormControl<boolean>;
|
||||
}>;
|
||||
sectionsResource = this.sectionService.list();
|
||||
|
||||
sections: Section[] = [];
|
||||
item: Device = new Device();
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
section: new FormControl<string | null>(null),
|
||||
enabled: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
item = computed(() => this.itemResource.value() ?? new Device());
|
||||
formModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): DeviceDetailFormData => {
|
||||
return {
|
||||
name: itemVal.name ?? '',
|
||||
section: itemVal.section.id ? itemVal.section.id : '',
|
||||
enabled: itemVal.enabled,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: Device; sections: Section[] };
|
||||
this.showItem(data.item);
|
||||
this.sections = data.sections;
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
showItem(item: Device) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
name: this.item.name ?? '',
|
||||
section: this.item.section.id ? this.item.section.id : '',
|
||||
enabled: this.item.enabled,
|
||||
});
|
||||
}
|
||||
sections = computed(() => this.sectionsResource.value() ?? []);
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
@@ -90,20 +82,20 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/devices');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
this.ser.delete(this.item().id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/devices');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -122,10 +114,12 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Device {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.section.id = formModel.section ?? '';
|
||||
this.item.enabled = formModel.enabled ?? true;
|
||||
return this.item;
|
||||
const formModel = this.formModel();
|
||||
return new Device({
|
||||
...this.item(),
|
||||
name: formModel.name ?? '',
|
||||
section: new Section({ id: formModel.section ?? '' }),
|
||||
enabled: formModel.enabled ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DeviceListResolver } from './device-list-resolver.service';
|
||||
|
||||
describe('DeviceListResolverService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DeviceListResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DeviceListResolver], (service: DeviceListResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Device } from '../core/device';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
export const deviceListResolver: ResolveFn<Device[]> = () => {
|
||||
return inject(DeviceService).list();
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { Device } from '../../core/device';
|
||||
|
||||
export class DeviceListDataSource extends DataSource<Device> {
|
||||
constructor(public data: Device[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Device[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
Add
|
||||
</a>
|
||||
</h2>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
import { Device } from '../../core/device';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { DeviceListDataSource } from './device-list-datasource';
|
||||
|
||||
import { DeviceService } from '../device.service';
|
||||
@Component({
|
||||
selector: 'app-device-list',
|
||||
templateUrl: './device-list.component.html',
|
||||
styleUrls: ['./device-list.component.css'],
|
||||
imports: [LocalTimePipe, MatButtonModule, MatIconModule, MatTableModule, RouterLink],
|
||||
})
|
||||
export class DeviceListComponent implements OnInit {
|
||||
export class DeviceListComponent {
|
||||
private deviceService = inject(DeviceService);
|
||||
private route = inject(ActivatedRoute);
|
||||
listResource = this.deviceService.list();
|
||||
|
||||
list: Device[] = [];
|
||||
dataSource: DeviceListDataSource = new DeviceListDataSource(this.list);
|
||||
dataSource = computed(() => this.listResource.value() ?? []);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'section', 'enabled', 'creationDate', 'last'];
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Device[] };
|
||||
this.list = data.list;
|
||||
});
|
||||
this.dataSource = new DeviceListDataSource(this.list);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DeviceResolver } from './device-resolver.service';
|
||||
|
||||
describe('DeviceResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DeviceResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DeviceResolver], (service: DeviceResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Device } from '../core/device';
|
||||
import { DeviceService } from './device.service';
|
||||
|
||||
export const deviceResolver: ResolveFn<Device> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(DeviceService).get(id);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -16,17 +16,15 @@ export class DeviceService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<Device> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Device>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Device>;
|
||||
get(id: Signal<string | null> | string | null): HttpResourceRef<Device | undefined> {
|
||||
return httpResource<Device>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
return idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Observable<Device[]> {
|
||||
return this.http
|
||||
.get<Device[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Device[]>;
|
||||
list(): HttpResourceRef<Device[] | undefined> {
|
||||
return httpResource<Device[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
save(device: Device): Observable<Device> {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { sectionListResolver } from '../sections/section-list.resolver';
|
||||
import { DeviceDetailComponent } from './device-detail/device-detail.component';
|
||||
import { deviceListResolver } from './device-list.resolver';
|
||||
import { DeviceListComponent } from './device-list/device-list.component';
|
||||
import { deviceResolver } from './device.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -15,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Devices',
|
||||
},
|
||||
resolve: {
|
||||
list: deviceListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Devices',
|
||||
},
|
||||
resolve: {
|
||||
item: deviceResolver,
|
||||
sections: sectionListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -38,9 +28,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Devices',
|
||||
},
|
||||
resolve: {
|
||||
item: deviceResolver,
|
||||
sections: sectionListResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { DiscountReportItem } from './discount-report-item';
|
||||
|
||||
export class DiscountReportDataSource extends DataSource<DiscountReportItem> {
|
||||
constructor(public data: DiscountReportItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<DiscountReportItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Discount Report</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button (click)="exportCsv()">
|
||||
<button type="button" mat-icon-button (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="print()">
|
||||
<button type="button" mat-icon-button (click)="print()">
|
||||
<mat-icon>print</mat-icon>
|
||||
</button>
|
||||
</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
@@ -16,7 +16,7 @@
|
||||
matInput
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDate.open()"
|
||||
formControlName="startDate"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
@@ -28,16 +28,16 @@
|
||||
matInput
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDate.open()"
|
||||
formControlName="finishDate"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
<button type="button" mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<mat-table #table [dataSource]="dataSource()" aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, computed, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -13,9 +13,13 @@ import moment from 'moment';
|
||||
|
||||
import { ToCsvService } from '../shared/to-csv.service';
|
||||
import { DiscountReport } from './discount-report';
|
||||
import { DiscountReportDataSource } from './discount-report-datasource';
|
||||
import { DiscountReportService } from './discount-report.service';
|
||||
|
||||
export interface DiscountReportFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-discount-report',
|
||||
templateUrl: './discount-report.component.html',
|
||||
@@ -23,52 +27,48 @@ import { DiscountReportService } from './discount-report.service';
|
||||
imports: [
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class DiscountReportComponent implements OnInit {
|
||||
export class DiscountReportComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private toCsv = inject(ToCsvService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(DiscountReportService);
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
info: DiscountReport = new DiscountReport();
|
||||
dataSource: DiscountReportDataSource = new DiscountReportDataSource(this.info.amounts);
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
}>;
|
||||
infoResource = this.ser.get(this.startDate, this.finishDate);
|
||||
|
||||
info = computed(() => this.infoResource.value() ?? new DiscountReport());
|
||||
dataSource = computed(() => this.info().amounts);
|
||||
formModel = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (infoVal): DiscountReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: moment(this.info().startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info().finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
startDate: new Date(),
|
||||
finishDate: new Date(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'amount'];
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: DiscountReport };
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
});
|
||||
this.dataSource = new DiscountReportDataSource(this.info.amounts);
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
this.router.navigate(['discount-report'], {
|
||||
@@ -80,7 +80,7 @@ export class DiscountReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
getInfo(): DiscountReport {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new DiscountReport({
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
@@ -89,12 +89,12 @@ export class DiscountReportComponent implements OnInit {
|
||||
}
|
||||
|
||||
print() {
|
||||
this.ser.print(this.info.startDate, this.info.finishDate).subscribe({
|
||||
this.ser.print(this.info().startDate, this.info().finishDate).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Successfully Printed', '');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export class DiscountReportComponent implements OnInit {
|
||||
Name: 'name',
|
||||
Amount: 'amount',
|
||||
};
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
|
||||
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource())], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DiscountReportResolver } from './discount-report-resolver.service';
|
||||
|
||||
describe('DiscountReportResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DiscountReportResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DiscountReportResolver], (service: DiscountReportResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { DiscountReport } from './discount-report';
|
||||
import { DiscountReportService } from './discount-report.service';
|
||||
|
||||
export const discountReportResolver: ResolveFn<DiscountReport> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') ?? null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') ?? null;
|
||||
return inject(DiscountReportService).get(startDate, finishDate);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { DiscountReportComponent } from './discount-report.component';
|
||||
import { discountReportResolver } from './discount-report.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Discount Report',
|
||||
},
|
||||
resolve: {
|
||||
info: discountReportResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -16,17 +16,22 @@ export class DiscountReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(startDate: string | null, finishDate: string | null): Observable<DiscountReport> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
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 (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
if (f !== null) {
|
||||
params = params.set('f', f);
|
||||
}
|
||||
return this.http
|
||||
.get<DiscountReport>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable<DiscountReport>;
|
||||
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);
|
||||
|
||||
@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>;
|
||||
}>;
|
||||
|
||||
item: GuestBook = new GuestBook();
|
||||
customers: Observable<Customer[]>;
|
||||
|
||||
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 }),
|
||||
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',
|
||||
});
|
||||
// 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))),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: GuestBook };
|
||||
this.showItem(data.item);
|
||||
});
|
||||
itemResource = this.ser.get(this.id, this.t);
|
||||
|
||||
item = computed(() => this.itemResource.value() ?? new GuestBook());
|
||||
|
||||
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(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
}),
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { guestBookDetailResolver } from './guest-book-detail.resolver';
|
||||
import { GuestBookDetailComponent } from './guest-book-detail/guest-book-detail.component';
|
||||
import { guestBookListResolver } from './guest-book-list.resolver';
|
||||
import { GuestBookListComponent } from './guest-book-list/guest-book-list.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
list: guestBookListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,9 +21,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
item: guestBookDetailResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -37,8 +29,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
item: guestBookDetailResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,10 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, Signal, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { GuestBook } from './guest-book';
|
||||
import { GuestBook, GuestBookType } from './guest-book';
|
||||
import { GuestBookList } from './guest-book-list';
|
||||
|
||||
const httpOptions = {
|
||||
@@ -19,19 +19,25 @@ export class GuestBookService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null, type: string): Observable<GuestBook> {
|
||||
const options = { params: new HttpParams().set('t', type) };
|
||||
const getUrl: string = id === null ? url : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<GuestBook>(getUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<GuestBook>;
|
||||
get(
|
||||
id: Signal<string | null> | string | null,
|
||||
type: Signal<GuestBookType> | GuestBookType | string = '',
|
||||
): HttpResourceRef<GuestBook | undefined> {
|
||||
return httpResource<GuestBook>(() => {
|
||||
const idVal = typeof id === 'function' ? id() : id;
|
||||
const typeVal = typeof type === 'function' ? type() : type;
|
||||
const options = { params: new HttpParams().set('t', typeVal) };
|
||||
const getUrl: string = idVal === null || idVal === undefined ? url : `${url}/${idVal}`;
|
||||
return { url: getUrl, ...options };
|
||||
});
|
||||
}
|
||||
|
||||
list(date: string | null): Observable<GuestBookList> {
|
||||
const options = { params: new HttpParams().set('q', date === null ? '' : date) };
|
||||
return this.http
|
||||
.get<GuestBookList>(`${url}/list`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<GuestBookList>;
|
||||
list(date: Signal<string | null> | string | null): HttpResourceRef<GuestBookList | undefined> {
|
||||
return httpResource<GuestBookList>(() => {
|
||||
const dateVal = typeof date === 'function' ? date() : date;
|
||||
const options = { params: new HttpParams().set('q', dateVal === null ? '' : dateVal) };
|
||||
return { url: `${url}/list`, ...options };
|
||||
});
|
||||
}
|
||||
|
||||
save(guestBook: GuestBook): Observable<GuestBook> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type GuestBookType = 'walk_in' | 'booking';
|
||||
|
||||
export class GuestBook {
|
||||
id: string | undefined;
|
||||
serial: number;
|
||||
@@ -9,7 +11,7 @@ export class GuestBook {
|
||||
notes: string | null;
|
||||
bookingDate: string | null;
|
||||
arrivalDate: string | null;
|
||||
type: string | undefined;
|
||||
type: GuestBookType | undefined;
|
||||
lastEditDate: string | undefined;
|
||||
status?: string;
|
||||
tableId?: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Header / Footer</mat-label>
|
||||
<mat-select formControlName="id" (selectionChange)="show($event)">
|
||||
@for (s of list; track s) {
|
||||
<mat-select [formField]="form.id" (selectionChange)="show($event)">
|
||||
@for (s of list(); track s) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
@@ -14,10 +14,10 @@
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Text</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="text"></textarea>
|
||||
<textarea matInput matAutosizeMinRows="5" [formField]="form.text"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -9,92 +9,82 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { HeaderFooter } from './header-footer';
|
||||
import { HeaderFooterService } from './header-footer.service';
|
||||
|
||||
export interface HeaderFooterFormData {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-section-printer',
|
||||
templateUrl: './header-footer.component.html',
|
||||
styleUrls: ['./header-footer.component.css'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
],
|
||||
})
|
||||
export class HeaderFooterComponent implements OnInit {
|
||||
export class HeaderFooterComponent {
|
||||
@ViewChild('section', { static: true }) sectionElement?: ElementRef;
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private dialog = inject(MatDialog);
|
||||
private ser = inject(HeaderFooterService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
listResource = this.ser.list();
|
||||
|
||||
@ViewChild('section', { static: true }) sectionElement?: ElementRef;
|
||||
form: FormGroup<{
|
||||
id: FormControl<string>;
|
||||
text: FormControl<string>;
|
||||
}>;
|
||||
list = computed(() => this.listResource.value() ?? []);
|
||||
|
||||
list: HeaderFooter[] = [];
|
||||
id = '';
|
||||
|
||||
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;
|
||||
});
|
||||
formModel = linkedSignal({
|
||||
source: () => {
|
||||
const list = this.list();
|
||||
let currentId = this.id();
|
||||
if (!currentId && list.length > 0) {
|
||||
currentId = list[0].id;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: HeaderFooter[] };
|
||||
this.showItem(data.list);
|
||||
return { list, id: currentId };
|
||||
},
|
||||
computation: ({ list, id }): HeaderFooterFormData => {
|
||||
const val = list.find((v) => v.id === id);
|
||||
return {
|
||||
id: id ?? '',
|
||||
text: val?.text ?? '',
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showItem(list: HeaderFooter[]) {
|
||||
this.list = list;
|
||||
this.id = this.id ?? this.list[0].id;
|
||||
const val = this.list.find((v) => v.id === this.id) as HeaderFooter;
|
||||
this.form.setValue({
|
||||
id: this.id,
|
||||
text: val.text,
|
||||
});
|
||||
}
|
||||
form = form(this.formModel);
|
||||
|
||||
save() {
|
||||
this.ser.save(this.getItem()).subscribe({
|
||||
next: (result: HeaderFooter[]) => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.showItem(result);
|
||||
this.listResource.value.set(result);
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Error');
|
||||
error: (error: unknown) => {
|
||||
this.snackBar.open(error as string, 'Error');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getItem(): HeaderFooter {
|
||||
const formModel = this.form.value;
|
||||
const item = this.list.find((v) => v.id === this.id);
|
||||
const formModel = this.formModel();
|
||||
const item = this.list().find((v) => v.id === formModel.id);
|
||||
if (item === undefined) {
|
||||
return new HeaderFooter();
|
||||
}
|
||||
item.text = formModel.text ?? '';
|
||||
return item;
|
||||
const clonedItem = Object.assign(new HeaderFooter(), item);
|
||||
clonedItem.text = formModel.text ?? '';
|
||||
return clonedItem;
|
||||
}
|
||||
|
||||
show(val: MatSelectChange) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeaderFooterResolver } from './header-footer-resolver.service';
|
||||
|
||||
describe('HeaderFooterResolver', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [HeaderFooterResolver],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([HeaderFooterResolver], (service: HeaderFooterResolver) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { HeaderFooter } from './header-footer';
|
||||
import { HeaderFooterService } from './header-footer.service';
|
||||
|
||||
export const headerFooterResolver: ResolveFn<HeaderFooter[]> = () => {
|
||||
return inject(HeaderFooterService).list();
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { HeaderFooterComponent } from './header-footer.component';
|
||||
import { headerFooterResolver } from './header-footer.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Owner',
|
||||
},
|
||||
resolve: {
|
||||
list: headerFooterResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -23,8 +19,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Owner',
|
||||
},
|
||||
resolve: {
|
||||
list: headerFooterResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders, httpResource, HttpResourceRef } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
@@ -19,10 +19,8 @@ export class HeaderFooterService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<HeaderFooter[]> {
|
||||
return this.http.get<HeaderFooter[]>(url).pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<
|
||||
HeaderFooter[]
|
||||
>;
|
||||
list(): HttpResourceRef<HeaderFooter[] | undefined> {
|
||||
return httpResource<HeaderFooter[]>(() => url);
|
||||
}
|
||||
|
||||
save(item: HeaderFooter): Observable<HeaderFooter[]> {
|
||||
|
||||
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { MenuCategoryDetailComponent } from './menu-category-detail/menu-category-detail.component';
|
||||
import { menuCategoryListResolver } from './menu-category-list.resolver';
|
||||
import { MenuCategoryListComponent } from './menu-category-list/menu-category-list.component';
|
||||
import { menuCategoryResolver } from './menu-category.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Products',
|
||||
},
|
||||
resolve: {
|
||||
list: menuCategoryListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -25,9 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Products',
|
||||
},
|
||||
resolve: {
|
||||
item: menuCategoryResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -36,8 +28,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Products',
|
||||
},
|
||||
resolve: {
|
||||
item: menuCategoryResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
+10
-6
@@ -1,19 +1,23 @@
|
||||
<h2>Menu Category</h2>
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" />
|
||||
<input matInput #nameElement [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="isActive">Is Active?</mat-checkbox>
|
||||
<mat-checkbox [formField]="form.isActive">Is Active?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button [disabled]="item.isFixture" color="primary" class="" (click)="save()">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button [disabled]="item.isFixture" color="warn" (click)="confirmDelete()">Delete</button>
|
||||
<button type="button" mat-raised-button [disabled]="item().isFixture" color="primary" class="" (click)="save()">
|
||||
Save
|
||||
</button>
|
||||
@if (!!item().id) {
|
||||
<button type="button" mat-raised-button [disabled]="item().isFixture" color="warn" (click)="confirmDelete()">
|
||||
Delete
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
+33
-40
@@ -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();
|
||||
|
||||
constructor() {
|
||||
// Create form
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string>('', { nonNullable: true }),
|
||||
isActive: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
item = computed(() => this.itemResource.value() ?? new MenuCategory());
|
||||
formModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): MenuCategoryDetailFormData => {
|
||||
return {
|
||||
name: itemVal.name,
|
||||
isActive: itemVal.isActive,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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() {}
|
||||
}
|
||||
Loaded 100 of 350 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user