Feature: Voucher tags. This will eventually help in filtering ledger based on tags and also to group vouchers.
This commit is contained in:
@ -74,8 +74,7 @@
|
||||
<mat-icon>account_box</mat-icon>
|
||||
{{ name }}
|
||||
</button>
|
||||
}
|
||||
@if ((auth.currentUser | async) === null) {
|
||||
} @else {
|
||||
<a mat-button routerLink="/login">
|
||||
<mat-icon>account_box</mat-icon>
|
||||
Login</a
|
||||
|
||||
17
overlord/src/app/core/tag.service.spec.ts
Normal file
17
overlord/src/app/core/tag.service.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagService } from './tag.service';
|
||||
|
||||
describe('TagService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientModule],
|
||||
providers: [TagService],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([TagService], (service: TagService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
64
overlord/src/app/core/tag.service.ts
Normal file
64
overlord/src/app/core/tag.service.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { ErrorLoggerService } from './error-logger.service';
|
||||
import { Tag } from './tag';
|
||||
|
||||
const url = '/api/tags';
|
||||
const serviceName = 'TagService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TagService {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private log: ErrorLoggerService,
|
||||
) {}
|
||||
|
||||
get(id: string): Observable<Tag> {
|
||||
return this.http
|
||||
.get<Tag>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Get Tag'))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
list(): Observable<Tag[]> {
|
||||
return this.http
|
||||
.get<Tag[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'List Tag'))) as Observable<Tag[]>;
|
||||
}
|
||||
|
||||
autocomplete(query: string): Observable<Tag[]> {
|
||||
const options = { params: new HttpParams().set('q', query) };
|
||||
return this.http
|
||||
.get<Tag[]>(`${url}/query`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Tag[]>;
|
||||
}
|
||||
|
||||
delete(id: string): Observable<Tag> {
|
||||
return this.http
|
||||
.delete<Tag>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Delete Tag'))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
saveOrUpdate(tag: Tag): Observable<Tag> {
|
||||
if (!tag.id) {
|
||||
return this.save(tag);
|
||||
}
|
||||
return this.update(tag);
|
||||
}
|
||||
|
||||
save(tag: Tag): Observable<Tag> {
|
||||
return this.http
|
||||
.post<Tag>(`${url}`, tag)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Save Tag'))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
update(tag: Tag): Observable<Tag> {
|
||||
return this.http
|
||||
.put<Tag>(`${url}/${tag.id}`, tag)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Update Tag'))) as Observable<Tag>;
|
||||
}
|
||||
}
|
||||
10
overlord/src/app/core/tag.ts
Normal file
10
overlord/src/app/core/tag.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class Tag {
|
||||
id: string | null;
|
||||
name: string;
|
||||
|
||||
public constructor(init?: Partial<Tag>) {
|
||||
this.id = null;
|
||||
this.name = '';
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { EmployeeBenefit } from './employee-benefit';
|
||||
import { Incentive } from './incentive';
|
||||
import { Inventory } from './inventory';
|
||||
import { Journal } from './journal';
|
||||
import { Tag } from './tag';
|
||||
import { User } from './user';
|
||||
|
||||
export class Voucher {
|
||||
@ -22,6 +23,7 @@ export class Voucher {
|
||||
employeeBenefits: EmployeeBenefit[];
|
||||
incentives: Incentive[];
|
||||
files: DbFile[];
|
||||
tags: Tag[];
|
||||
creationDate: string;
|
||||
lastEditDate: string;
|
||||
user: User;
|
||||
@ -40,6 +42,7 @@ export class Voucher {
|
||||
this.employeeBenefits = [];
|
||||
this.incentives = [];
|
||||
this.files = [];
|
||||
this.tags = [];
|
||||
this.creationDate = '';
|
||||
this.lastEditDate = '';
|
||||
this.user = new User();
|
||||
|
||||
@ -110,6 +110,33 @@
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="narration"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Tag selection">
|
||||
@for (tag of voucher.tags; track tag) {
|
||||
<mat-chip-row (removed)="removeTag(tag)">
|
||||
{{ tag.name }}
|
||||
<button matChipRemove [attr.aria-label]="'remove ' + tag.name">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New Tag..."
|
||||
#tagInput
|
||||
formControlName="tags"
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matAutocomplete]="autoTag"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addTag($event)"
|
||||
/>
|
||||
<mat-autocomplete #autoTag="matAutocomplete" (optionSelected)="selectedTag($event)">
|
||||
@for (tag of tags | async; track tag) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-row justify-center items-stretch">
|
||||
@for (item of voucher.files; track item) {
|
||||
<div class="img-container" class="flex-auto mr-5 basis-1/5">
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
|
||||
import { round } from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { Account } from '../core/account';
|
||||
@ -15,6 +17,8 @@ import { AccountBalance } from '../core/account-balance';
|
||||
import { AccountService } from '../core/account.service';
|
||||
import { DbFile } from '../core/db-file';
|
||||
import { Journal } from '../core/journal';
|
||||
import { Tag } from '../core/tag';
|
||||
import { TagService } from '../core/tag.service';
|
||||
import { ToasterService } from '../core/toaster.service';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
@ -35,6 +39,8 @@ import { JournalDialogComponent } from './journal-dialog.component';
|
||||
export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('accountElement', { static: true }) accountElement?: ElementRef;
|
||||
@ViewChild('dateElement', { static: true }) dateElement?: ElementRef;
|
||||
@ViewChild('tagInput') tagInput?: ElementRef<HTMLInputElement>;
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public journalObservable = new BehaviorSubject<Journal[]>([]);
|
||||
dataSource: JournalDataSource = new JournalDataSource(this.journalObservable);
|
||||
form: FormGroup<{
|
||||
@ -45,9 +51,11 @@ export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: FormControl<string>;
|
||||
}>;
|
||||
narration: FormControl<string>;
|
||||
tags: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
voucher: Voucher = new Voucher();
|
||||
tags: Observable<Tag[]>;
|
||||
account: Account | null;
|
||||
accBal: AccountBalance | null = null;
|
||||
|
||||
@ -66,6 +74,7 @@ export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
public image: ImageService,
|
||||
private ser: VoucherService,
|
||||
private accountSer: AccountService,
|
||||
private tagSer: TagService,
|
||||
) {
|
||||
this.account = null;
|
||||
this.form = new FormGroup({
|
||||
@ -76,6 +85,7 @@ export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: new FormControl('', { nonNullable: true }),
|
||||
}),
|
||||
narration: new FormControl('', { nonNullable: true }),
|
||||
tags: new FormControl(''),
|
||||
});
|
||||
this.accBal = null;
|
||||
// Setup Account Autocomplete
|
||||
@ -84,6 +94,12 @@ export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))),
|
||||
);
|
||||
this.tags = this.form.controls.tags.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
map((tag: string | Tag | null) => (tag === null ? '' : typeof tag !== 'string' ? tag.name.toLowerCase() : tag)),
|
||||
switchMap((tag: string) => this.tagSer.autocomplete(tag)),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@ -150,6 +166,7 @@ export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: '',
|
||||
},
|
||||
narration: this.voucher.narration,
|
||||
tags: '',
|
||||
});
|
||||
this.dataSource = new JournalDataSource(this.journalObservable);
|
||||
this.journalObservable.next(this.voucher.journals);
|
||||
@ -338,4 +355,38 @@ export class JournalComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const index = this.voucher.files.indexOf(file);
|
||||
this.voucher.files.splice(index, 1);
|
||||
}
|
||||
|
||||
removeTag(tag: Tag): void {
|
||||
const index = this.voucher.tags.indexOf(tag);
|
||||
|
||||
if (index >= 0) {
|
||||
this.voucher.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our tag
|
||||
if (value) {
|
||||
this.voucher.tags.push(new Tag({ name: value }));
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
|
||||
selectedTag(event: MatAutocompleteSelectedEvent): void {
|
||||
const tag = event.option.value as Tag;
|
||||
const index = this.voucher.tags.findIndex((t) => (tag.id === null ? t.name === tag.name : t.id === tag.id));
|
||||
if (index === -1) {
|
||||
this.voucher.tags.push(tag);
|
||||
}
|
||||
if (this.tagInput) {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
@ -49,6 +50,7 @@ export const MY_FORMATS = {
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
||||
@ -113,6 +113,33 @@
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="narration"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Tag selection">
|
||||
@for (tag of voucher.tags; track tag) {
|
||||
<mat-chip-row (removed)="removeTag(tag)">
|
||||
{{ tag.name }}
|
||||
<button matChipRemove [attr.aria-label]="'remove ' + tag.name">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New Tag..."
|
||||
#tagInput
|
||||
formControlName="tags"
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matAutocomplete]="autoTag"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addTag($event)"
|
||||
/>
|
||||
<mat-autocomplete #autoTag="matAutocomplete" (optionSelected)="selectedTag($event)">
|
||||
@for (tag of tags | async; track tag) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-row justify-center items-stretch">
|
||||
@for (item of voucher.files; track item) {
|
||||
<div class="img-container" class="flex-auto mr-5 basis-1/5">
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
|
||||
import { round } from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { Account } from '../core/account';
|
||||
@ -15,6 +17,8 @@ import { AccountBalance } from '../core/account-balance';
|
||||
import { AccountService } from '../core/account.service';
|
||||
import { DbFile } from '../core/db-file';
|
||||
import { Journal } from '../core/journal';
|
||||
import { Tag } from '../core/tag';
|
||||
import { TagService } from '../core/tag.service';
|
||||
import { ToasterService } from '../core/toaster.service';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
@ -35,6 +39,8 @@ import { PaymentDialogComponent } from './payment-dialog.component';
|
||||
export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('accountElement', { static: true }) accountElement?: ElementRef;
|
||||
@ViewChild('dateElement', { static: true }) dateElement?: ElementRef;
|
||||
@ViewChild('tagInput') tagInput?: ElementRef<HTMLInputElement>;
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public journalObservable = new BehaviorSubject<Journal[]>([]);
|
||||
dataSource: PaymentDataSource = new PaymentDataSource(this.journalObservable);
|
||||
form: FormGroup<{
|
||||
@ -46,6 +52,7 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: FormControl<string>;
|
||||
}>;
|
||||
narration: FormControl<string>;
|
||||
tags: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
paymentAccounts: Account[] = [];
|
||||
@ -57,6 +64,7 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
displayedColumns = ['account', 'amount', 'action'];
|
||||
|
||||
accounts: Observable<Account[]>;
|
||||
tags: Observable<Tag[]>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -69,6 +77,7 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
public image: ImageService,
|
||||
private ser: VoucherService,
|
||||
private accountSer: AccountService,
|
||||
private tagSer: TagService,
|
||||
) {
|
||||
this.account = null;
|
||||
this.form = new FormGroup({
|
||||
@ -80,6 +89,7 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: new FormControl('', { nonNullable: true }),
|
||||
}),
|
||||
narration: new FormControl('', { nonNullable: true }),
|
||||
tags: new FormControl(''),
|
||||
});
|
||||
this.accBal = null;
|
||||
// Listen to Account Autocomplete Change
|
||||
@ -88,6 +98,12 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))),
|
||||
);
|
||||
this.tags = this.form.controls.tags.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
map((tag: string | Tag | null) => (tag === null ? '' : typeof tag !== 'string' ? tag.name.toLowerCase() : tag)),
|
||||
switchMap((tag: string) => this.tagSer.autocomplete(tag)),
|
||||
);
|
||||
// Listen to Payment Account Change
|
||||
this.form.controls.paymentAccount.valueChanges.subscribe((x) =>
|
||||
this.router.navigate([], {
|
||||
@ -165,6 +181,7 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: '',
|
||||
},
|
||||
narration: this.voucher.narration,
|
||||
tags: '',
|
||||
});
|
||||
this.dataSource = new PaymentDataSource(this.journalObservable);
|
||||
this.updateView();
|
||||
@ -349,4 +366,38 @@ export class PaymentComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const index = this.voucher.files.indexOf(file);
|
||||
this.voucher.files.splice(index, 1);
|
||||
}
|
||||
|
||||
removeTag(tag: Tag): void {
|
||||
const index = this.voucher.tags.indexOf(tag);
|
||||
|
||||
if (index >= 0) {
|
||||
this.voucher.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our tag
|
||||
if (value) {
|
||||
this.voucher.tags.push(new Tag({ name: value }));
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
|
||||
selectedTag(event: MatAutocompleteSelectedEvent): void {
|
||||
const tag = event.option.value as Tag;
|
||||
const index = this.voucher.tags.findIndex((t) => (tag.id === null ? t.name === tag.name : t.id === tag.id));
|
||||
if (index === -1) {
|
||||
this.voucher.tags.push(tag);
|
||||
}
|
||||
if (this.tagInput) {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
@ -49,6 +50,7 @@ export const MY_FORMATS = {
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
||||
@ -51,8 +51,6 @@
|
||||
</div>
|
||||
@if (item.productGroup?.nutritional ?? false) {
|
||||
<h2>Nutritional Information</h2>
|
||||
}
|
||||
@if (item.productGroup?.nutritional ?? false) {
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-form-field class="flex-auto mr-5">
|
||||
<mat-label>Protein</mat-label>
|
||||
@ -94,8 +92,6 @@
|
||||
}
|
||||
@if (item.productGroup?.iceCream ?? false) {
|
||||
<h2>Ice Cream Information</h2>
|
||||
}
|
||||
@if (item.productGroup?.iceCream ?? false) {
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-form-field class="flex-auto mr-5">
|
||||
<mat-label>MSNF</mat-label>
|
||||
|
||||
@ -148,6 +148,33 @@
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="narration"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Tag selection">
|
||||
@for (tag of voucher.tags; track tag) {
|
||||
<mat-chip-row (removed)="removeTag(tag)">
|
||||
{{ tag.name }}
|
||||
<button matChipRemove [attr.aria-label]="'remove ' + tag.name">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New Tag..."
|
||||
#tagInput
|
||||
formControlName="tags"
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matAutocomplete]="autoTag"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addTag($event)"
|
||||
/>
|
||||
<mat-autocomplete #autoTag="matAutocomplete" (optionSelected)="selectedTag($event)">
|
||||
@for (tag of tags | async; track tag) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-row justify-center items-stretch mr-5">
|
||||
@for (item of voucher.files; track item) {
|
||||
<div class="img-container" class="flex-auto mr-5 basis-1/5">
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
|
||||
import { round } from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { Account } from '../core/account';
|
||||
@ -17,6 +19,8 @@ import { Batch } from '../core/batch';
|
||||
import { BatchService } from '../core/batch.service';
|
||||
import { DbFile } from '../core/db-file';
|
||||
import { Inventory } from '../core/inventory';
|
||||
import { Tag } from '../core/tag';
|
||||
import { TagService } from '../core/tag.service';
|
||||
import { ToasterService } from '../core/toaster.service';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
@ -38,6 +42,8 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
@ViewChild('accountElement', { static: true }) accountElement?: ElementRef;
|
||||
@ViewChild('batchElement', { static: true }) batchElement?: ElementRef;
|
||||
@ViewChild('dateElement', { static: true }) dateElement?: ElementRef;
|
||||
@ViewChild('tagInput') tagInput?: ElementRef<HTMLInputElement>;
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public inventoryObservable = new BehaviorSubject<Inventory[]>([]);
|
||||
dataSource: PurchaseReturnDataSource = new PurchaseReturnDataSource(this.inventoryObservable);
|
||||
form: FormGroup<{
|
||||
@ -49,6 +55,7 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
quantity: FormControl<string>;
|
||||
}>;
|
||||
narration: FormControl<string>;
|
||||
tags: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
voucher: Voucher = new Voucher();
|
||||
@ -58,6 +65,7 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
displayedColumns = ['product', 'quantity', 'rate', 'tax', 'discount', 'amount', 'action'];
|
||||
|
||||
accounts: Observable<Account[]>;
|
||||
tags: Observable<Tag[]>;
|
||||
batches: Observable<Batch[]>;
|
||||
|
||||
constructor(
|
||||
@ -72,6 +80,7 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
private ser: VoucherService,
|
||||
private batchSer: BatchService,
|
||||
private accountSer: AccountService,
|
||||
private tagSer: TagService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(new Date(), { nonNullable: true }),
|
||||
@ -82,6 +91,7 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
quantity: new FormControl<string>('', { nonNullable: true }),
|
||||
}),
|
||||
narration: new FormControl('', { nonNullable: true }),
|
||||
tags: new FormControl(''),
|
||||
});
|
||||
// Listen to Account Autocomplete Change
|
||||
this.accounts = this.form.controls.account.valueChanges.pipe(
|
||||
@ -89,6 +99,12 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))),
|
||||
);
|
||||
this.tags = this.form.controls.tags.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
map((tag: string | Tag | null) => (tag === null ? '' : typeof tag !== 'string' ? tag.name.toLowerCase() : tag)),
|
||||
switchMap((tag: string) => this.tagSer.autocomplete(tag)),
|
||||
);
|
||||
// Listen to Batch Autocomplete Change
|
||||
this.batches = this.form.controls.addRow.controls.batch.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
@ -166,6 +182,7 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
quantity: '',
|
||||
},
|
||||
narration: this.voucher.narration,
|
||||
tags: '',
|
||||
});
|
||||
this.dataSource = new PurchaseReturnDataSource(this.inventoryObservable);
|
||||
this.updateView();
|
||||
@ -351,4 +368,38 @@ export class PurchaseReturnComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
const index = this.voucher.files.indexOf(file);
|
||||
this.voucher.files.splice(index, 1);
|
||||
}
|
||||
|
||||
removeTag(tag: Tag): void {
|
||||
const index = this.voucher.tags.indexOf(tag);
|
||||
|
||||
if (index >= 0) {
|
||||
this.voucher.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our tag
|
||||
if (value) {
|
||||
this.voucher.tags.push(new Tag({ name: value }));
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
|
||||
selectedTag(event: MatAutocompleteSelectedEvent): void {
|
||||
const tag = event.option.value as Tag;
|
||||
const index = this.voucher.tags.findIndex((t) => (tag.id === null ? t.name === tag.name : t.id === tag.id));
|
||||
if (index === -1) {
|
||||
this.voucher.tags.push(tag);
|
||||
}
|
||||
if (this.tagInput) {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
@ -49,6 +50,7 @@ export const MY_FORMATS = {
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
||||
@ -160,6 +160,33 @@
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="narration"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Tag selection">
|
||||
@for (tag of voucher.tags; track tag) {
|
||||
<mat-chip-row (removed)="removeTag(tag)">
|
||||
{{ tag.name }}
|
||||
<button matChipRemove [attr.aria-label]="'remove ' + tag.name">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New Tag..."
|
||||
#tagInput
|
||||
formControlName="tags"
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matAutocomplete]="autoTag"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addTag($event)"
|
||||
/>
|
||||
<mat-autocomplete #autoTag="matAutocomplete" (optionSelected)="selectedTag($event)">
|
||||
@for (tag of tags | async; track tag) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-row justify-center items-stretch mr-5">
|
||||
@for (item of voucher.files; track item) {
|
||||
<div class="img-container" class="flex-auto mr-5 basis-1/5">
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
|
||||
import { round } from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { Account } from '../core/account';
|
||||
@ -18,6 +20,8 @@ import { DbFile } from '../core/db-file';
|
||||
import { Inventory } from '../core/inventory';
|
||||
import { Product } from '../core/product';
|
||||
import { ProductSku } from '../core/product-sku';
|
||||
import { Tag } from '../core/tag';
|
||||
import { TagService } from '../core/tag.service';
|
||||
import { ToasterService } from '../core/toaster.service';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
@ -40,6 +44,8 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('accountElement', { static: true }) accountElement?: ElementRef;
|
||||
@ViewChild('productElement', { static: true }) productElement?: ElementRef;
|
||||
@ViewChild('dateElement', { static: true }) dateElement?: ElementRef;
|
||||
@ViewChild('tagInput') tagInput?: ElementRef<HTMLInputElement>;
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public inventoryObservable = new BehaviorSubject<Inventory[]>([]);
|
||||
dataSource: PurchaseDataSource = new PurchaseDataSource(this.inventoryObservable);
|
||||
form: FormGroup<{
|
||||
@ -54,6 +60,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
discount: FormControl<string>;
|
||||
}>;
|
||||
narration: FormControl<string>;
|
||||
tags: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
voucher: Voucher = new Voucher();
|
||||
@ -63,6 +70,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
displayedColumns = ['product', 'quantity', 'rate', 'tax', 'discount', 'amount', 'action'];
|
||||
|
||||
accounts: Observable<Account[]>;
|
||||
tags: Observable<Tag[]>;
|
||||
products: Observable<ProductSku[]>;
|
||||
|
||||
constructor(
|
||||
@ -77,6 +85,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private ser: VoucherService,
|
||||
private productSer: ProductService,
|
||||
private accountSer: AccountService,
|
||||
private tagSer: TagService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(new Date(), { nonNullable: true }),
|
||||
@ -90,6 +99,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
discount: new FormControl('', { nonNullable: true }),
|
||||
}),
|
||||
narration: new FormControl('', { nonNullable: true }),
|
||||
tags: new FormControl(''),
|
||||
});
|
||||
this.accBal = null;
|
||||
// Listen to Account Autocomplete Change
|
||||
@ -98,6 +108,12 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))),
|
||||
);
|
||||
this.tags = this.form.controls.tags.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
map((tag: string | Tag | null) => (tag === null ? '' : typeof tag !== 'string' ? tag.name.toLowerCase() : tag)),
|
||||
switchMap((tag: string) => this.tagSer.autocomplete(tag)),
|
||||
);
|
||||
// Listen to Product Autocomplete Change
|
||||
this.products = this.form.controls.addRow.controls.product.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
@ -184,6 +200,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
discount: '',
|
||||
},
|
||||
narration: this.voucher.narration,
|
||||
tags: '',
|
||||
});
|
||||
this.dataSource = new PurchaseDataSource(this.inventoryObservable);
|
||||
this.updateView();
|
||||
@ -393,4 +410,38 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const index = this.voucher.files.indexOf(file);
|
||||
this.voucher.files.splice(index, 1);
|
||||
}
|
||||
|
||||
removeTag(tag: Tag): void {
|
||||
const index = this.voucher.tags.indexOf(tag);
|
||||
|
||||
if (index >= 0) {
|
||||
this.voucher.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our tag
|
||||
if (value) {
|
||||
this.voucher.tags.push(new Tag({ name: value }));
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
|
||||
selectedTag(event: MatAutocompleteSelectedEvent): void {
|
||||
const tag = event.option.value as Tag;
|
||||
const index = this.voucher.tags.findIndex((t) => (tag.id === null ? t.name === tag.name : t.id === tag.id));
|
||||
if (index === -1) {
|
||||
this.voucher.tags.push(tag);
|
||||
}
|
||||
if (this.tagInput) {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
@ -49,6 +50,7 @@ export const MY_FORMATS = {
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
||||
@ -113,6 +113,33 @@
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="narration"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Tag selection">
|
||||
@for (tag of voucher.tags; track tag) {
|
||||
<mat-chip-row (removed)="removeTag(tag)">
|
||||
{{ tag.name }}
|
||||
<button matChipRemove [attr.aria-label]="'remove ' + tag.name">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New Tag..."
|
||||
#tagInput
|
||||
formControlName="tags"
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matAutocomplete]="autoTag"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addTag($event)"
|
||||
/>
|
||||
<mat-autocomplete #autoTag="matAutocomplete" (optionSelected)="selectedTag($event)">
|
||||
@for (tag of tags | async; track tag) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-row justify-center items-stretch">
|
||||
@for (item of voucher.files; track item) {
|
||||
<div class="img-container" class="flex-auto mr-5 basis-1/5">
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
|
||||
import { round } from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { Account } from '../core/account';
|
||||
@ -15,6 +17,8 @@ import { AccountBalance } from '../core/account-balance';
|
||||
import { AccountService } from '../core/account.service';
|
||||
import { DbFile } from '../core/db-file';
|
||||
import { Journal } from '../core/journal';
|
||||
import { Tag } from '../core/tag';
|
||||
import { TagService } from '../core/tag.service';
|
||||
import { ToasterService } from '../core/toaster.service';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
@ -35,6 +39,8 @@ import { ReceiptDialogComponent } from './receipt-dialog.component';
|
||||
export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('accountElement', { static: true }) accountElement?: ElementRef;
|
||||
@ViewChild('dateElement', { static: true }) dateElement?: ElementRef;
|
||||
@ViewChild('tagInput') tagInput?: ElementRef<HTMLInputElement>;
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public journalObservable = new BehaviorSubject<Journal[]>([]);
|
||||
dataSource: ReceiptDataSource = new ReceiptDataSource(this.journalObservable);
|
||||
form: FormGroup<{
|
||||
@ -46,6 +52,7 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: FormControl<string>;
|
||||
}>;
|
||||
narration: FormControl<string>;
|
||||
tags: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
receiptAccounts: Account[] = [];
|
||||
@ -57,6 +64,7 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
displayedColumns = ['account', 'amount', 'action'];
|
||||
|
||||
accounts: Observable<Account[]>;
|
||||
tags: Observable<Tag[]>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -69,6 +77,7 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
public image: ImageService,
|
||||
private ser: VoucherService,
|
||||
private accountSer: AccountService,
|
||||
private tagSer: TagService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(new Date(), { nonNullable: true }),
|
||||
@ -79,6 +88,7 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: new FormControl('', { nonNullable: true }),
|
||||
}),
|
||||
narration: new FormControl('', { nonNullable: true }),
|
||||
tags: new FormControl(''),
|
||||
});
|
||||
this.accBal = null;
|
||||
// Listen to Account Autocomplete Change
|
||||
@ -87,6 +97,12 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))),
|
||||
);
|
||||
this.tags = this.form.controls.tags.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
map((tag: string | Tag | null) => (tag === null ? '' : typeof tag !== 'string' ? tag.name.toLowerCase() : tag)),
|
||||
switchMap((tag: string) => this.tagSer.autocomplete(tag)),
|
||||
);
|
||||
// Listen to Receipt Account Change
|
||||
this.form.controls.receiptAccount.valueChanges.subscribe((x) =>
|
||||
this.router.navigate([], {
|
||||
@ -164,6 +180,7 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
amount: '',
|
||||
},
|
||||
narration: this.voucher.narration,
|
||||
tags: '',
|
||||
});
|
||||
this.dataSource = new ReceiptDataSource(this.journalObservable);
|
||||
this.updateView();
|
||||
@ -348,4 +365,38 @@ export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const index = this.voucher.files.indexOf(file);
|
||||
this.voucher.files.splice(index, 1);
|
||||
}
|
||||
|
||||
removeTag(tag: Tag): void {
|
||||
const index = this.voucher.tags.indexOf(tag);
|
||||
|
||||
if (index >= 0) {
|
||||
this.voucher.tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our tag
|
||||
if (value) {
|
||||
this.voucher.tags.push(new Tag({ name: value }));
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
|
||||
selectedTag(event: MatAutocompleteSelectedEvent): void {
|
||||
const tag = event.option.value as Tag;
|
||||
const index = this.voucher.tags.findIndex((t) => (tag.id === null ? t.name === tag.name : t.id === tag.id));
|
||||
if (index === -1) {
|
||||
this.voucher.tags.push(tag);
|
||||
}
|
||||
if (this.tagInput) {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
this.form.controls.tags.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
@ -49,6 +50,7 @@ export const MY_FORMATS = {
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
||||
Reference in New Issue
Block a user