Consolidated commit message to v22 signals
This commit is contained in:
1 parent
6403d25d3e
commit
df289b20b8
443 files changed
+16058
-17332
No files matched your search
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { paymentAccountsResolver } from './payment-accounts.resolver';
|
||||
|
||||
describe('paymentAccountsResolver', () => {
|
||||
const executeResolver: ResolveFn<Account[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => paymentAccountsResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { AccountService } from '../core/account.service';
|
||||
|
||||
export const paymentAccountsResolver: ResolveFn<Account[]> = () => {
|
||||
return inject(AccountService).paymentAutocomplete('');
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Journal } from '../core/journal';
|
||||
|
||||
export class PaymentDataSource extends DataSource<Journal> {
|
||||
constructor(private data: Observable<Journal[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Journal[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 mat-dialog-title>Edit Payment Entry</h1>
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="form">
|
||||
<form [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-2-3">
|
||||
<mat-label>Account</mat-label>
|
||||
@@ -9,29 +9,27 @@
|
||||
matInput
|
||||
#accountElement
|
||||
[matAutocomplete]="auto"
|
||||
formControlName="account"
|
||||
[formField]="form.account"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (accBal) {
|
||||
@if (accBal()) {
|
||||
<mat-hint>
|
||||
Balance as on Date: <strong>{{ accBal.date | currency: 'INR' | accounting }}</strong> / Final balance:
|
||||
<strong>{{ accBal.total | currency: 'INR' | accounting }}</strong>
|
||||
<div class="balance">
|
||||
Balance as on Date: <strong>{{ accBal()?.date | currency: 'INR' | accounting }}</strong
|
||||
><br />
|
||||
Total Balance: <strong>{{ accBal()?.total | currency: 'INR' | accounting }}</strong>
|
||||
</div>
|
||||
</mat-hint>
|
||||
}
|
||||
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn">
|
||||
@for (account of accounts(); track account.id) {
|
||||
<mat-option [value]="account">{{ account.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
autoActiveFirstOption
|
||||
(optionSelected)="accountSelected($event)"
|
||||
[displayWith]="displayFn"
|
||||
>
|
||||
@for (account of accounts | async; track account) {
|
||||
<mat-option [value]="account">{{ account.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
<mat-form-field class="flex-auto basis-1-3">
|
||||
<mat-label>Amount</mat-label>
|
||||
<input type="text" matInput formControlName="amount" autocomplete="off" />
|
||||
<input type="text" matInput [formField]="form.amount" autocomplete="off" />
|
||||
<span matTextPrefix>₹ </span>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { CdkScrollableModule } from '@angular/cdk/scrolling';
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, computed, inject, linkedSignal } from '@angular/core';
|
||||
import { form, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { AccountBalance } from '../core/account-balance';
|
||||
import { AccountService } from '../core/account.service';
|
||||
import { Journal } from '../core/journal';
|
||||
import { AccountingPipe } from '../shared/accounting.pipe';
|
||||
import { MathService } from '../shared/math.service';
|
||||
|
||||
export interface PaymentDialogFormData {
|
||||
account: Account | string | null;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment-dialog',
|
||||
templateUrl: './payment-dialog.component.html',
|
||||
@@ -25,18 +26,17 @@ import { MathService } from '../shared/math.service';
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
CdkScrollableModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatAutocompleteModule,
|
||||
MatOptionModule,
|
||||
MatButtonModule,
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
AccountingPipe,
|
||||
],
|
||||
})
|
||||
export class PaymentDialogComponent implements OnInit {
|
||||
export class PaymentDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<PaymentDialogComponent>>(MatDialogRef);
|
||||
data = inject<{
|
||||
journal: Journal;
|
||||
@@ -45,56 +45,52 @@ export class PaymentDialogComponent implements OnInit {
|
||||
|
||||
private math = inject(MathService);
|
||||
private accountSer = inject(AccountService);
|
||||
private cd = inject(ChangeDetectorRef);
|
||||
|
||||
accounts: Observable<Account[]>;
|
||||
form: FormGroup<{
|
||||
account: FormControl<string | null>;
|
||||
amount: FormControl<string>;
|
||||
}>;
|
||||
formModel = linkedSignal<
|
||||
{
|
||||
journal: Journal;
|
||||
date: string;
|
||||
},
|
||||
PaymentDialogFormData
|
||||
>({
|
||||
source: () => this.data,
|
||||
computation: (data: { journal: Journal; date: string }) => ({
|
||||
account: data.journal.account,
|
||||
amount: `${data.journal.amount}`,
|
||||
}),
|
||||
});
|
||||
|
||||
account: Account = new Account();
|
||||
accBal: AccountBalance | null = null;
|
||||
form = form(this.formModel);
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
account: new FormControl(''),
|
||||
amount: new FormControl('', { nonNullable: true }),
|
||||
});
|
||||
this.accBal = null;
|
||||
// Setup Account Autocomplete
|
||||
this.accounts = this.form.controls.account.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))),
|
||||
);
|
||||
}
|
||||
accountSearch = computed(() => {
|
||||
const v = this.form.account().value();
|
||||
return typeof v === 'string' ? v : null;
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.form.setValue({
|
||||
account: this.data.journal.account.name,
|
||||
amount: `${this.data.journal.amount}`,
|
||||
});
|
||||
this.account = this.data.journal.account;
|
||||
}
|
||||
accountsResource = this.accountSer.autocomplete(this.accountSearch);
|
||||
accounts = computed(() => this.accountsResource.value() ?? []);
|
||||
|
||||
displayFn(account?: Account | string): string {
|
||||
return !account ? '' : typeof account === 'string' ? account : account.name;
|
||||
}
|
||||
balanceDate = computed(() => this.data.date);
|
||||
accountId = computed((): string | null => {
|
||||
const account = this.form.account().value();
|
||||
return typeof account === 'string' ? null : (account?.id ?? null);
|
||||
});
|
||||
|
||||
accountSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const account = event.option.value;
|
||||
this.account = account;
|
||||
this.accountSer.balance(account.id as string, this.data.date).subscribe((v) => {
|
||||
this.accBal = v;
|
||||
this.cd.detectChanges();
|
||||
});
|
||||
accBalResource = this.accountSer.balance(this.accountId, this.balanceDate);
|
||||
accBal = computed(() => this.accBalResource.value() ?? null);
|
||||
|
||||
displayFn(account?: Account): string {
|
||||
return account ? account.name : '';
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
const formValue = this.form.value;
|
||||
const amount = this.math.parseAmount(formValue.amount, 2);
|
||||
this.data.journal.account = this.account;
|
||||
const formValue = this.form().value();
|
||||
const account = typeof formValue.account === 'string' ? null : formValue.account;
|
||||
if (!account || !account.id) {
|
||||
return;
|
||||
}
|
||||
const amount = this.math.parseAmount(formValue.amount ?? '0', 2);
|
||||
this.data.journal.account = account;
|
||||
this.data.journal.amount = amount;
|
||||
this.dialogRef.close(this.data.journal);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Payment</span>
|
||||
<mat-icon matSuffix (click)="voucher.isStarred = !voucher.isStarred" class="pointer" [class.gold]="voucher.isStarred">
|
||||
{{ voucher.isStarred ? 'star' : 'star_border' }}
|
||||
<mat-icon
|
||||
matSuffix
|
||||
(click)="form.isStarred().value.set(!form.isStarred())"
|
||||
class="pointer"
|
||||
[class.gold]="form.isStarred()"
|
||||
>
|
||||
{{ form.isStarred() ? 'star' : 'star_border' }}
|
||||
</mat-icon>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput [matDatepicker]="date" formControlName="date" autocomplete="off" #dateElement />
|
||||
<input matInput [matDatepicker]="date" [formField]="form.date" autocomplete="off" #dateElement />
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-select formControlName="paymentAccount">
|
||||
@for (account of paymentAccounts; track account) {
|
||||
<mat-select [formField]="form.paymentAccount">
|
||||
@for (account of paymentAccounts(); track account.id) {
|
||||
<mat-option [value]="account.id">
|
||||
{{ account.name }}
|
||||
</mat-option>
|
||||
@@ -24,12 +29,12 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-1-5">
|
||||
<mat-label>Payment Amount</mat-label>
|
||||
<input type="text" matInput formControlName="paymentAmount" />
|
||||
<input type="number" matInput [formField]="form.paymentAmount" />
|
||||
<span matTextPrefix>₹ </span>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div formGroupName="addRow" class="row-container">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-55">
|
||||
<mat-label>Account</mat-label>
|
||||
<input
|
||||
@@ -37,143 +42,149 @@
|
||||
matInput
|
||||
#accountElement
|
||||
[matAutocomplete]="auto"
|
||||
formControlName="account"
|
||||
[formField]="form.addRow.account"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (accBal) {
|
||||
@if (accBal()) {
|
||||
<mat-hint>
|
||||
Balance as on Date: <strong>{{ accBal.date | currency: 'INR' | accounting }}</strong> / Final balance:
|
||||
<strong>{{ accBal.total | currency: 'INR' | accounting }}</strong>
|
||||
Balance as on Date: <strong>{{ accBal()?.date | currency: 'INR' | accounting }}</strong> / Final balance:
|
||||
<strong>{{ accBal()?.total | currency: 'INR' | accounting }}</strong>
|
||||
</mat-hint>
|
||||
}
|
||||
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn">
|
||||
@for (account of accounts(); track account.id) {
|
||||
<mat-option [value]="account">{{ account.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
autoActiveFirstOption
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="accountSelected($event)"
|
||||
>
|
||||
@for (account of accounts | async; track account) {
|
||||
<mat-option [value]="account">{{ account.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
<mat-form-field class="flex-auto basis-1-4">
|
||||
<mat-label>Amount</mat-label>
|
||||
<input type="text" matInput formControlName="amount" autocomplete="off" />
|
||||
<input type="text" matInput [formField]="form.addRow.amount" autocomplete="off" />
|
||||
<span matTextPrefix>₹ </span>
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto basis-1-5">Add</button>
|
||||
</div>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Account Column -->
|
||||
<ng-container matColumnDef="account">
|
||||
<mat-header-cell *matHeaderCellDef>Account</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.account.name }}</mat-cell>
|
||||
</ng-container>
|
||||
@if (itemResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (itemResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="form.journals().value() ?? []" matSort (matSortChange)="sortData($event)">
|
||||
<!-- Account Column -->
|
||||
<ng-container matColumnDef="account">
|
||||
<mat-header-cell *matHeaderCellDef>Account</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.account.name }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Column -->
|
||||
<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)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Action Column -->
|
||||
<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)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" formControlName="narration"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<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>
|
||||
<div class="row-container">
|
||||
@for (item of voucher.files; track item) {
|
||||
<div class="img-container" class="flex-auto basis-1-5">
|
||||
<img [src]="item.thumbnail" alt="img" (click)="zoomImage(item)" (keyup.enter)="zoomImage(item)" tabindex="0" />
|
||||
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<div class="img-container" class="flex flex-auto basis-1-5">
|
||||
<label ng-href for="fileUp">
|
||||
<img
|
||||
alt="add"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="fileUp"
|
||||
multiple
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
(change)="image.detectFiles($event, voucher)"
|
||||
/>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Narration</mat-label>
|
||||
<textarea matInput matAutosizeMinRows="5" [formField]="form.narration"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Tag selection">
|
||||
@for (tag of form.tagList().value(); 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
|
||||
[formField]="form.tagInput"
|
||||
[matChipInputFor]="chipGrid"
|
||||
[matAutocomplete]="autoTag"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addTag()"
|
||||
/>
|
||||
<mat-autocomplete #autoTag="matAutocomplete" (optionSelected)="selectedTag($event)">
|
||||
@for (tag of tags(); track tag.id) {
|
||||
<mat-option [value]="tag">{{ tag.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
@for (item of form.files().value(); track item) {
|
||||
<div class="img-container" class="flex-auto basis-1-5">
|
||||
<img
|
||||
[src]="item.thumbnail"
|
||||
alt="img"
|
||||
(click)="zoomImage(item)"
|
||||
(keyup.enter)="zoomImage(item)"
|
||||
tabindex="0"
|
||||
/>
|
||||
<button mat-icon-button class="overlay" (click)="deleteImage(item)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<div class="img-container" class="flex flex-auto basis-1-5">
|
||||
<label ng-href for="fileUp">
|
||||
<img
|
||||
alt="add"
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iI2VlZSIvPjx0ZXh0IHRleHQtYW5jaG9yPSJtaWRkbGUiIHg9IjUwIiB5PSI3NSIgc3R5bGU9ImZpbGw6I2FhYTtmb250LXdlaWdodDpib2xkO2ZvbnQtc2l6ZToxMDBweDtmb250LWZhbWlseTpBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtkb21pbmFudC1iYXNlbGluZTpjZW50cmFsIj4rPC90ZXh0Pjwvc3ZnPg=="
|
||||
/>
|
||||
</label>
|
||||
<input type="file" id="fileUp" multiple accept="image/*" style="display: none" (change)="detectFiles($event)" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="!canSave()" class="">
|
||||
{{ voucher.id ? 'Update' : 'Save' }}
|
||||
{{ id() ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
@if (voucher.id) {
|
||||
<button mat-raised-button (click)="post()" [disabled]="voucher.posted || !auth.allowed('post-vouchers')" class="">
|
||||
{{ voucher.posted ? 'Posted' : 'Post' }}
|
||||
@if (id()) {
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="post()"
|
||||
[disabled]="form.posted().controlValue() || !auth.allowed('post-vouchers')"
|
||||
class=""
|
||||
>
|
||||
{{ form.posted().controlValue() ? 'Posted' : 'Post' }}
|
||||
</button>
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()" [disabled]="!canSave()">Delete</button>
|
||||
}
|
||||
<span class="spacer"></span>
|
||||
@if (voucher.id) {
|
||||
@if (id()) {
|
||||
<span class="flex">
|
||||
Created on <strong>{{ voucher.creationDate | localTime }}</strong
|
||||
> and Last Edited on <strong>{{ voucher.lastEditDate | localTime }}</strong
|
||||
> by <strong>{{ voucher.user.name }}</strong
|
||||
>. {{ voucher.poster ? 'Posted by ' + voucher.poster : '' }}
|
||||
Created on <strong>{{ form.creationDate().value() | localTime }}</strong
|
||||
> and Last Edited on <strong>{{ form.lastEditDate().value() | localTime }}</strong
|
||||
> by <strong>{{ form.userName() }}</strong
|
||||
>.
|
||||
{{ form.posted() ? 'Posted by ' + form.poster() : '' }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@@ -1,19 +1,20 @@
|
||||
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
inject,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
computed,
|
||||
effect,
|
||||
signal,
|
||||
input,
|
||||
linkedSignal,
|
||||
afterNextRender,
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { form, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -22,18 +23,17 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { round } from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
import { zip } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { Account } from '../core/account';
|
||||
import { AccountBalance } from '../core/account-balance';
|
||||
import { AccountService } from '../core/account.service';
|
||||
import { CostCentre } from '../core/cost-centre';
|
||||
import { DbFile } from '../core/db-file';
|
||||
import { Journal } from '../core/journal';
|
||||
import { User } from '../core/user';
|
||||
@@ -41,22 +41,52 @@ import { Voucher } from '../core/voucher';
|
||||
import { VoucherService } from '../core/voucher.service';
|
||||
import { AccountingPipe } from '../shared/accounting.pipe';
|
||||
import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { ImageDialogComponent } from '../shared/image-dialog/image-dialog.component';
|
||||
import { ImageService } from '../shared/image.service';
|
||||
import { LocalTimePipe } from '../shared/local-time.pipe';
|
||||
import { MathService } from '../shared/math.service';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { Tag } from '../tag/tag';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { PaymentDataSource } from './payment-datasource';
|
||||
import { PaymentDialogComponent } from './payment-dialog.component';
|
||||
|
||||
export interface PaymentItemFormData {
|
||||
id: string | undefined;
|
||||
amount: number;
|
||||
account: Account;
|
||||
costCentre: CostCentre | null;
|
||||
}
|
||||
|
||||
export interface PaymentFormModel {
|
||||
isStarred: boolean;
|
||||
date: Date;
|
||||
paymentAccount: string;
|
||||
paymentAmount: number;
|
||||
addRow: {
|
||||
account: Account | string | null;
|
||||
amount: string;
|
||||
};
|
||||
journals: PaymentItemFormData[];
|
||||
narration: string;
|
||||
tagInput: string | Tag | null;
|
||||
tagList: Tag[];
|
||||
files: DbFile[];
|
||||
posted: boolean;
|
||||
creationDate: string;
|
||||
lastEditDate: string;
|
||||
userName: string;
|
||||
poster: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-payment',
|
||||
templateUrl: './payment.component.html',
|
||||
styleUrls: ['./payment.component.css'],
|
||||
imports: [
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
@@ -67,14 +97,20 @@ import { PaymentDialogComponent } from './payment-dialog.component';
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatChipsModule,
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
AccountingPipe,
|
||||
LocalTimePipe,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class PaymentComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class PaymentComponent {
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
d = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
a = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
type = signal('Payment');
|
||||
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
@@ -84,17 +120,89 @@ export class PaymentComponent implements OnInit, AfterViewInit {
|
||||
private ser = inject(VoucherService);
|
||||
private accountSer = inject(AccountService);
|
||||
private tagSer = inject(TagService);
|
||||
private cd = inject(ChangeDetectorRef);
|
||||
|
||||
@ViewChild('accountElement', { static: true }) accountElement!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('dateElement', { static: true }) dateElement!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('tagInput') tagInput?: ElementRef<HTMLInputElement>;
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
|
||||
itemResource = this.ser.getVoucher(this.id, this.type, this.a, this.d);
|
||||
item = linkedSignal(() => this.itemResource.value() ?? new Voucher());
|
||||
|
||||
formModel = linkedSignal<Voucher, PaymentFormModel>({
|
||||
source: this.item,
|
||||
computation: (voucher): PaymentFormModel => {
|
||||
const pj = voucher.journals.find((x) => x.debit === -1);
|
||||
const debitJournals = voucher.journals.filter((x) => x.debit === 1);
|
||||
const paymentAmount =
|
||||
pj?.amount ??
|
||||
round(
|
||||
debitJournals.reduce((acc, j) => acc + j.amount, 0),
|
||||
2,
|
||||
);
|
||||
return {
|
||||
isStarred: voucher.isStarred,
|
||||
date: voucher.date ? moment(voucher.date, 'DD-MMM-YYYY').toDate() : new Date(),
|
||||
paymentAccount: pj?.account?.id ?? this.a() ?? '',
|
||||
paymentAmount,
|
||||
addRow: {
|
||||
account: null as Account | string | null,
|
||||
amount: '',
|
||||
},
|
||||
journals: debitJournals.map((j) => ({
|
||||
id: j.id,
|
||||
amount: j.amount,
|
||||
account: j.account,
|
||||
costCentre: j.costCentre,
|
||||
})),
|
||||
narration: voucher.narration ?? '',
|
||||
tagInput: null as string | Tag | null,
|
||||
tagList: [...voucher.tags],
|
||||
files: [...voucher.files],
|
||||
posted: voucher.posted,
|
||||
creationDate: voucher.creationDate,
|
||||
lastEditDate: voucher.lastEditDate,
|
||||
userName: voucher.user?.name ?? '',
|
||||
poster: voucher.poster ?? '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
displayedColumns = ['account', 'amount', 'action'];
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
|
||||
accountSearch = computed(() => {
|
||||
const v = this.form.addRow.account().value();
|
||||
return typeof v === 'string' ? v : null;
|
||||
});
|
||||
|
||||
accountsResource = this.accountSer.autocomplete(this.accountSearch);
|
||||
accounts = computed(() => this.accountsResource.value() ?? []);
|
||||
|
||||
tagQuery = computed(() => {
|
||||
const tag = this.form.tagInput().value();
|
||||
return tag === null ? '' : typeof tag !== 'string' ? tag.name.toLowerCase() : tag;
|
||||
});
|
||||
|
||||
tagsResource = this.tagSer.autocomplete(this.tagQuery);
|
||||
tags = computed(() => this.tagsResource.value() ?? []);
|
||||
|
||||
balanceDate = computed(() => moment(this.form.date().value()).format('DD-MMM-YYYY'));
|
||||
accountId = computed((): string | null => {
|
||||
const account = this.form.addRow.account().value();
|
||||
return typeof account === 'string' ? null : (account?.id ?? null);
|
||||
});
|
||||
|
||||
accBalResource = this.accountSer.balance(this.accountId, this.balanceDate);
|
||||
accBal = computed(() => this.accBalResource.value() ?? null);
|
||||
|
||||
paymentAccountsResource = this.accountSer.paymentAutocomplete(signal(''));
|
||||
paymentAccounts = computed(() => this.paymentAccountsResource.value() ?? []);
|
||||
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.dateElement.nativeElement.focus();
|
||||
this.dateElement.nativeElement.select();
|
||||
this.form().focusBoundControl();
|
||||
}
|
||||
|
||||
@HostListener('window:keydown.f5', ['$event'])
|
||||
@@ -107,8 +215,8 @@ export class PaymentComponent implements OnInit, AfterViewInit {
|
||||
event.preventDefault();
|
||||
this.router.navigate(['receipt'], {
|
||||
queryParams: {
|
||||
a: this.form.controls.paymentAccount.value,
|
||||
d: this.form.controls.date.value.format('DD-MMM-YYYY'),
|
||||
a: this.formModel().paymentAccount,
|
||||
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
@@ -119,8 +227,7 @@ export class PaymentComponent implements OnInit, AfterViewInit {
|
||||
event.preventDefault();
|
||||
this.router.navigate(['journal'], {
|
||||
queryParams: {
|
||||
a: this.form.controls.paymentAccount.value,
|
||||
d: this.form.controls.date.value.format('DD-MMM-YYYY'),
|
||||
d: moment(this.formModel().date).format('DD-MMM-YYYY'),
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
@@ -137,222 +244,174 @@ export class PaymentComponent implements OnInit, AfterViewInit {
|
||||
@HostListener('window:keydown.control.p', ['$event'])
|
||||
postListner(event: Event) {
|
||||
event.preventDefault();
|
||||
if (this.voucher.id && !this.voucher.posted && this.auth.allowed('post-vouchers')) {
|
||||
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
|
||||
this.post();
|
||||
}
|
||||
}
|
||||
|
||||
separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
public journalObservable = new BehaviorSubject<Journal[]>([]);
|
||||
dataSource: PaymentDataSource = new PaymentDataSource(this.journalObservable);
|
||||
form: FormGroup<{
|
||||
date: FormControl<moment.Moment>;
|
||||
paymentAccount: FormControl<string>;
|
||||
paymentAmount: FormControl<number>;
|
||||
addRow: FormGroup<{
|
||||
account: FormControl<string | null>;
|
||||
amount: FormControl<string>;
|
||||
}>;
|
||||
narration: FormControl<string>;
|
||||
tags: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
paymentAccounts: Account[] = [];
|
||||
paymentJournal: Journal = new Journal();
|
||||
voucher: Voucher = new Voucher();
|
||||
account: Account | null;
|
||||
accBal: AccountBalance | null = null;
|
||||
|
||||
displayedColumns = ['account', 'amount', 'action'];
|
||||
|
||||
accounts: Observable<Account[]>;
|
||||
tags: Observable<Tag[]>;
|
||||
|
||||
constructor() {
|
||||
this.account = null;
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
paymentAccount: new FormControl('', { nonNullable: true }),
|
||||
paymentAmount: new FormControl({ value: 0, disabled: true }, { nonNullable: true }),
|
||||
addRow: new FormGroup({
|
||||
account: new FormControl(''),
|
||||
amount: new FormControl('', { nonNullable: true }),
|
||||
}),
|
||||
narration: new FormControl('', { nonNullable: true }),
|
||||
tags: new FormControl(''),
|
||||
afterNextRender(() => {
|
||||
this.form.addRow.account().focusBoundControl();
|
||||
});
|
||||
this.accBal = null;
|
||||
// Listen to Account Autocomplete Change
|
||||
this.accounts = this.form.controls.addRow.controls.account.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
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 Date Change
|
||||
this.form.controls.date.valueChanges.subscribe((x) =>
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { d: x.format('DD-MMM-YYYY') },
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
);
|
||||
// Listen to Payment Account Change
|
||||
this.form.controls.paymentAccount.valueChanges.subscribe((x) =>
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { a: x },
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { voucher: Voucher; paymentAccounts: Account[] };
|
||||
effect(() => {
|
||||
const d = this.formModel().date;
|
||||
if (!d) return;
|
||||
|
||||
this.paymentAccounts = data.paymentAccounts;
|
||||
this.loadVoucher(data.voucher);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('d', moment(d).format('DD-MMM-YYYY'));
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.focusAccount();
|
||||
}
|
||||
effect(() => {
|
||||
const a = this.formModel().paymentAccount;
|
||||
if (!a) return;
|
||||
|
||||
loadVoucher(voucher: Voucher) {
|
||||
this.voucher = voucher;
|
||||
[this.paymentJournal] = this.voucher.journals.filter((x) => x.debit === -1);
|
||||
this.form.setValue({
|
||||
date: moment(this.voucher.date, 'DD-MMM-YYYY'),
|
||||
paymentAccount: this.paymentJournal.account.id ?? '',
|
||||
paymentAmount: this.paymentJournal.amount,
|
||||
addRow: {
|
||||
account: '',
|
||||
amount: '',
|
||||
},
|
||||
narration: this.voucher.narration,
|
||||
tags: '',
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('a', a);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
});
|
||||
this.dataSource = new PaymentDataSource(this.journalObservable);
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
focusAccount() {
|
||||
setTimeout(() => {
|
||||
this.accountElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
addRow() {
|
||||
const amount = this.math.parseAmount(this.form.value.addRow?.amount ?? '0', 2);
|
||||
const debit = 1;
|
||||
if (this.account === null || amount <= 0) {
|
||||
const formValue = this.form().value().addRow;
|
||||
if (!formValue) return;
|
||||
const amount = this.math.parseAmount(formValue.amount ?? '0', 2);
|
||||
const account = formValue.account as Account;
|
||||
|
||||
if (!account || typeof account === 'string' || amount <= 0) {
|
||||
return;
|
||||
}
|
||||
const oldFiltered = this.voucher.journals.filter((x) => x.account.id === (this.account as Account).id);
|
||||
const old = oldFiltered.length ? oldFiltered[0] : null;
|
||||
if (old && (old.debit === -1 || old.id === this.paymentJournal.id)) {
|
||||
return;
|
||||
}
|
||||
if (old) {
|
||||
old.amount += amount;
|
||||
} else {
|
||||
this.voucher.journals.push(
|
||||
new Journal({
|
||||
debit,
|
||||
amount,
|
||||
account: this.account,
|
||||
costCentre: null,
|
||||
}),
|
||||
const currentJournals = this.formModel().journals;
|
||||
const oldIndex = currentJournals.findIndex((x) => x.account.id === account.id);
|
||||
let updatedJournals: PaymentItemFormData[];
|
||||
if (oldIndex >= 0) {
|
||||
updatedJournals = currentJournals.map((j, i) =>
|
||||
i === oldIndex ? { ...j, amount: round(j.amount + amount, 2) } : j,
|
||||
);
|
||||
} else {
|
||||
updatedJournals = [...currentJournals, { id: undefined, account, amount, costCentre: null }];
|
||||
}
|
||||
this.resetAddRow();
|
||||
this.updateView();
|
||||
const totalPaymentAmount = round(
|
||||
updatedJournals.reduce((acc, j) => acc + j.amount, 0),
|
||||
2,
|
||||
);
|
||||
this.formModel.update((m) => ({
|
||||
...m,
|
||||
journals: updatedJournals,
|
||||
paymentAmount: totalPaymentAmount,
|
||||
}));
|
||||
this.form.addRow.account().value.set(null);
|
||||
this.form.addRow.amount().value.set('');
|
||||
this.form.addRow.account().focusBoundControl();
|
||||
}
|
||||
|
||||
resetAddRow() {
|
||||
this.form.controls.addRow.reset({ account: null, amount: '' });
|
||||
this.account = null;
|
||||
this.accBal = null;
|
||||
setTimeout(() => {
|
||||
this.accountElement.nativeElement.focus();
|
||||
}, 0);
|
||||
deleteRow(row: PaymentItemFormData) {
|
||||
const updatedJournals = this.formModel().journals.filter((j) => j !== row);
|
||||
const totalPaymentAmount = round(
|
||||
updatedJournals.reduce((acc, j) => acc + j.amount, 0),
|
||||
2,
|
||||
);
|
||||
this.formModel.update((m) => ({
|
||||
...m,
|
||||
journals: updatedJournals,
|
||||
paymentAmount: totalPaymentAmount,
|
||||
}));
|
||||
this.form.addRow.account().value.set(null);
|
||||
this.form.addRow.account().focusBoundControl();
|
||||
}
|
||||
|
||||
updateView() {
|
||||
const journals = this.voucher.journals.filter((x) => x.debit === 1);
|
||||
this.journalObservable.next(journals);
|
||||
this.paymentJournal.amount = round(Math.abs(journals.map((x) => x.amount).reduce((p, c) => p + c, 0)), 2);
|
||||
this.form.controls.paymentAmount.setValue(this.paymentJournal.amount);
|
||||
}
|
||||
|
||||
editRow(row: Journal) {
|
||||
editRow(row: PaymentItemFormData) {
|
||||
const dialogRef = this.dialog.open(PaymentDialogComponent, {
|
||||
width: '750px',
|
||||
data: {
|
||||
journal: { ...row },
|
||||
date: moment(this.form.value.date).format('DD-MMM-YYYY'),
|
||||
journal: new Journal({
|
||||
id: row.id,
|
||||
debit: 1,
|
||||
amount: row.amount,
|
||||
account: row.account,
|
||||
costCentre: row.costCentre,
|
||||
}),
|
||||
date: moment(this.formModel().date).format('DD-MMM-YYYY'),
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean | Journal) => {
|
||||
if (!result) {
|
||||
if (!result || typeof result === 'boolean') {
|
||||
return;
|
||||
}
|
||||
const j = result as Journal;
|
||||
if (
|
||||
j.account.id !== row.account.id &&
|
||||
this.voucher.journals.filter((x) => x.account.id === j.account.id).length
|
||||
) {
|
||||
const j = result;
|
||||
if (j.account.id !== row.account.id && this.formModel().journals.some((x) => x.account.id === j.account.id)) {
|
||||
this.snackBar.open('Account already exists in journals', 'Danger');
|
||||
return;
|
||||
}
|
||||
Object.assign(row, j);
|
||||
this.updateView();
|
||||
const updatedJournals = this.formModel().journals.map((item) =>
|
||||
item === row
|
||||
? {
|
||||
...item,
|
||||
account: j.account,
|
||||
amount: j.amount,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
const totalPaymentAmount = round(
|
||||
updatedJournals.reduce((acc, jItem) => acc + jItem.amount, 0),
|
||||
2,
|
||||
);
|
||||
this.formModel.update((m) => ({
|
||||
...m,
|
||||
journals: updatedJournals,
|
||||
paymentAmount: totalPaymentAmount,
|
||||
}));
|
||||
this.form.addRow.account().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
deleteRow(row: Journal) {
|
||||
this.voucher.journals.splice(this.voucher.journals.indexOf(row), 1);
|
||||
this.updateView();
|
||||
getVoucher(): Voucher {
|
||||
const m = this.formModel();
|
||||
const base = this.item();
|
||||
const v = new Voucher({ ...base });
|
||||
v.date = moment(m.date).format('DD-MMM-YYYY');
|
||||
v.narration = m.narration ?? '';
|
||||
|
||||
const existingPj = base.journals.find((x) => x.debit === -1);
|
||||
const paymentAccountObj =
|
||||
this.paymentAccounts().find((a) => a.id === m.paymentAccount) ?? new Account({ id: m.paymentAccount });
|
||||
const pj = new Journal({
|
||||
id: existingPj?.id,
|
||||
debit: -1,
|
||||
amount: m.paymentAmount,
|
||||
account: paymentAccountObj,
|
||||
costCentre: existingPj?.costCentre ?? null,
|
||||
});
|
||||
|
||||
const debitJournals = m.journals.map(
|
||||
(j) => new Journal({ id: j.id, debit: 1, amount: j.amount, account: j.account, costCentre: j.costCentre }),
|
||||
);
|
||||
|
||||
v.journals = [pj, ...debitJournals];
|
||||
v.tags = [...m.tagList];
|
||||
v.files = [...m.files];
|
||||
return v;
|
||||
}
|
||||
|
||||
canSave() {
|
||||
if (!this.voucher.id) {
|
||||
if (!this.id()) {
|
||||
return true;
|
||||
}
|
||||
if (this.voucher.posted && this.auth.allowed('edit-posted-vouchers')) {
|
||||
return true;
|
||||
if (this.form.posted().value() && !this.auth.allowed('edit-posted-vouchers')) {
|
||||
return false;
|
||||
}
|
||||
return this.voucher.user.id === (this.auth.user as User).id || this.auth.allowed("edit-other-user's-vouchers");
|
||||
}
|
||||
|
||||
post() {
|
||||
this.ser.post(this.voucher.id as string).subscribe({
|
||||
next: (result) => {
|
||||
this.loadVoucher(result);
|
||||
this.snackBar.open('Voucher Posted', 'Success');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
const user = this.auth.user as User;
|
||||
return this.item().user?.id === user?.id || this.auth.allowed("edit-other-user's-vouchers");
|
||||
}
|
||||
|
||||
save() {
|
||||
const voucher: Voucher = this.getVoucher();
|
||||
this.ser.saveOrUpdate(voucher).subscribe({
|
||||
this.ser.saveOrUpdate(this.getVoucher()).subscribe({
|
||||
next: (result) => {
|
||||
this.snackBar.open('', 'Success');
|
||||
if (voucher.id === result.id) {
|
||||
this.loadVoucher(result);
|
||||
this.snackBar.open('Voucher Saved', undefined, { duration: 2000 });
|
||||
if (this.item().id === result.id) {
|
||||
this.item.set(result);
|
||||
} else {
|
||||
this.router.navigate(['/payment', result.id]);
|
||||
}
|
||||
@@ -363,96 +422,120 @@ export class PaymentComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
getVoucher(): Voucher {
|
||||
const formModel = this.form.value;
|
||||
this.voucher.date = moment(formModel.date).format('DD-MMM-YYYY');
|
||||
this.paymentJournal.account.id = formModel.paymentAccount;
|
||||
this.voucher.narration = formModel.narration ?? '';
|
||||
return this.voucher;
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.voucher.id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigate(['/payment'], { replaceUrl: true });
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
post() {
|
||||
const id = this.id();
|
||||
if (id) {
|
||||
this.ser.post(id).subscribe({
|
||||
next: (result) => {
|
||||
this.snackBar.open('Voucher Posted', undefined, { duration: 2000 });
|
||||
this.item.set(result);
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
confirmDelete(): void {
|
||||
const id = this.id();
|
||||
if (!id) return;
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '250px',
|
||||
data: { title: 'Delete Voucher?', content: 'Are you sure? This cannot be undone.' },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayFn(account?: Account | string): string {
|
||||
return !account ? '' : typeof account === 'string' ? account : account.name;
|
||||
delete() {
|
||||
const id = this.id();
|
||||
if (id) {
|
||||
this.ser.delete(id).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Voucher Deleted', undefined, { duration: 2000 });
|
||||
this.router.navigate(['/payment'], { replaceUrl: true });
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
accountSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
const account = event.option.value;
|
||||
this.account = account;
|
||||
const date = moment(this.form.value.date).format('DD-MMM-YYYY');
|
||||
this.accountSer.balance(account.id as string, date).subscribe((v) => {
|
||||
this.accBal = v;
|
||||
this.cd.detectChanges();
|
||||
});
|
||||
displayFn(account?: Account): string {
|
||||
return account ? account.name : '';
|
||||
}
|
||||
|
||||
zoomImage(file: DbFile) {
|
||||
this.dialog.open(ImageDialogComponent, {
|
||||
width: '750px',
|
||||
data: file.resized,
|
||||
data: file,
|
||||
panelClass: 'full-picture',
|
||||
});
|
||||
}
|
||||
|
||||
deleteImage(file: DbFile) {
|
||||
const index = this.voucher.files.indexOf(file);
|
||||
this.voucher.files.splice(index, 1);
|
||||
this.formModel.update((m) => ({
|
||||
...m,
|
||||
files: m.files.filter((f) => f !== file),
|
||||
}));
|
||||
}
|
||||
|
||||
detectFiles(event: Event) {
|
||||
const fileList = (event.target as HTMLInputElement).files;
|
||||
if (!fileList) return;
|
||||
for (const file of Array.from(fileList)) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const src = (e.target as FileReader).result as string;
|
||||
zip(this.image.resizeImage(src, 100, 150), this.image.resizeImage(src, 825, 1170)).subscribe(
|
||||
([thumbnail, resized]) => {
|
||||
this.formModel.update((m) => ({
|
||||
...m,
|
||||
files: [...m.files, new DbFile({ thumbnail, resized })],
|
||||
}));
|
||||
},
|
||||
);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(tag: Tag): void {
|
||||
const index = this.voucher.tags.indexOf(tag);
|
||||
|
||||
if (index >= 0) {
|
||||
this.voucher.tags.splice(index, 1);
|
||||
}
|
||||
this.formModel.update((m) => ({
|
||||
...m,
|
||||
tagList: m.tagList.filter((t) => t !== tag),
|
||||
}));
|
||||
}
|
||||
|
||||
addTag(event: MatChipInputEvent): void {
|
||||
const value = (event.value || '').trim();
|
||||
|
||||
// Add our tag
|
||||
if (value) {
|
||||
this.voucher.tags.push(new Tag({ name: value }));
|
||||
addTag(): void {
|
||||
const query = this.form.tagInput().value();
|
||||
if (!query) return;
|
||||
const val = (query as string).trim().toLowerCase();
|
||||
if (!val) return;
|
||||
const existing = this.formModel().tagList;
|
||||
const old = existing.find((t) => t.name.toLowerCase() === val);
|
||||
if (!old) {
|
||||
this.formModel.update((m) => ({ ...m, tagList: [...m.tagList, { id: null, name: val }] }));
|
||||
}
|
||||
|
||||
// Clear the input value
|
||||
event.chipInput!.clear();
|
||||
|
||||
this.form.controls.tags.setValue(null);
|
||||
this.form.tagInput().value.set(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);
|
||||
const existing = this.formModel().tagList;
|
||||
const old = existing.find((t) => t.name.toLowerCase() === tag.name.toLowerCase());
|
||||
if (!old) {
|
||||
this.formModel.update((m) => ({ ...m, tagList: [...m.tagList, tag] }));
|
||||
}
|
||||
if (this.tagInput) {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
this.form.controls.tags.setValue(null);
|
||||
this.form.tagInput().value.set(null);
|
||||
}
|
||||
|
||||
sortData(sort: Sort) {
|
||||
this.sortActive.set(sort.active);
|
||||
this.sortDirection.set(sort.direction);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Voucher } from '../core/voucher';
|
||||
import { paymentResolver } from './payment.resolver';
|
||||
|
||||
describe('paymentResolver', () => {
|
||||
const executeResolver: ResolveFn<Voucher> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => paymentResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Voucher } from '../core/voucher';
|
||||
import { VoucherService } from '../core/voucher.service';
|
||||
|
||||
export const paymentResolver: ResolveFn<Voucher> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
const account = route.queryParamMap.get('a') || null;
|
||||
const date = route.queryParamMap.get('d') || null;
|
||||
if (id !== null) {
|
||||
return inject(VoucherService).get(id, 'Payment');
|
||||
}
|
||||
return inject(VoucherService).getOfType('Payment', account, date);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { paymentAccountsResolver } from './payment-accounts.resolver';
|
||||
import { PaymentComponent } from './payment.component';
|
||||
import { paymentResolver } from './payment.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'journal',
|
||||
},
|
||||
resolve: {
|
||||
paymentAccounts: paymentAccountsResolver,
|
||||
voucher: paymentResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'paramsChange',
|
||||
},
|
||||
{
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'journal',
|
||||
},
|
||||
resolve: {
|
||||
paymentAccounts: paymentAccountsResolver,
|
||||
voucher: paymentResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'paramsChange',
|
||||
},
|
||||
];
|
||||
Reference in new issue
Block a user