Feature: Filtering and tagging now available in Ledger
This commit is contained in:
1 parent
336a8f59c5
commit
c0eca311c7
40 files changed
+973
-72
No files matched your search
@@ -2,24 +2,41 @@ import { DataSource } from '@angular/cdk/collections';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { merge, Observable, of as observableOf } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { LedgerItem } from './ledger-item';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class LedgerDataSource extends DataSource<LedgerItem> {
|
||||
public data: LedgerItem[] = [];
|
||||
private tags: string[] = [];
|
||||
public paginator?: MatPaginator;
|
||||
public sort?: MatSort;
|
||||
public debit = 0;
|
||||
public credit = 0;
|
||||
public running = 0;
|
||||
|
||||
constructor(
|
||||
public data: LedgerItem[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
public dataObs: Observable<LedgerItem[]>,
|
||||
public filter: Observable<string[]>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<LedgerItem[]> {
|
||||
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
|
||||
const d = this.dataObs.pipe(
|
||||
tap((x) => {
|
||||
this.data = x;
|
||||
}),
|
||||
);
|
||||
const f = this.filter.pipe(
|
||||
tap((x) => {
|
||||
this.tags = x;
|
||||
}),
|
||||
);
|
||||
if (this.paginator) {
|
||||
dataMutations.push((this.paginator as MatPaginator).page);
|
||||
}
|
||||
@@ -27,18 +44,48 @@ export class LedgerDataSource extends DataSource<LedgerItem> {
|
||||
dataMutations.push((this.sort as MatSort).sortChange);
|
||||
}
|
||||
|
||||
// Set the paginators length
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.length;
|
||||
}
|
||||
|
||||
return merge(observableOf(this.data), ...dataMutations).pipe(
|
||||
map(() => this.getPagedData(this.getSortedData([...this.data]))),
|
||||
);
|
||||
return merge(d, f, ...dataMutations)
|
||||
.pipe(
|
||||
map(() => this.getFilteredData()),
|
||||
tap((x: LedgerItem[]) => {
|
||||
if (this.paginator) {
|
||||
this.paginator.length = x.length;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(map((x: LedgerItem[]) => this.getPagedData(this.getSortedData(x))));
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
calculateTotals(data: LedgerItem[]) {
|
||||
this.debit = 0;
|
||||
this.credit = 0;
|
||||
this.running = 0;
|
||||
data.forEach((item) => {
|
||||
if (item.type !== 'Opening Balance') {
|
||||
this.debit += item.debit;
|
||||
this.credit += item.credit;
|
||||
if (item.posted) {
|
||||
this.running += item.debit - item.credit;
|
||||
}
|
||||
} else {
|
||||
this.running += item.debit - item.credit;
|
||||
}
|
||||
item.running = this.running;
|
||||
});
|
||||
}
|
||||
|
||||
private getFilteredData(): LedgerItem[] {
|
||||
const fData = this.data.filter(
|
||||
(x) =>
|
||||
(x.tags.length === 0 && this.tags.indexOf('(None)') !== -1) ||
|
||||
x.tags.filter((t) => this.tags.includes(t)).length > 0,
|
||||
);
|
||||
this.calculateTotals(fData);
|
||||
return fData;
|
||||
}
|
||||
|
||||
private getPagedData(data: LedgerItem[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
|
||||
@@ -9,6 +9,7 @@ export class LedgerItem {
|
||||
running: number;
|
||||
posted: boolean;
|
||||
url: string;
|
||||
tags: string[];
|
||||
|
||||
public constructor(init?: Partial<LedgerItem>) {
|
||||
this.date = '';
|
||||
@@ -21,6 +22,7 @@ export class LedgerItem {
|
||||
this.running = 0;
|
||||
this.posted = true;
|
||||
this.url = '';
|
||||
this.tags = [];
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@@ -55,12 +55,42 @@
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1/5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto basis-4/5 mr-5">
|
||||
<mat-label>Tags</mat-label>
|
||||
<mat-select formControlName="tags" multiple (selectionChange)="tagChanges($event.value)">
|
||||
@for (tag of tags; track tag) {
|
||||
<mat-option [value]="tag">{{ tag }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button tabindex="-1" extended class="flex-auto basis-1/5" (click)="labels()">
|
||||
<mat-icon>label</mat-icon>
|
||||
Manage Labels
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.date }}</mat-cell>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox
|
||||
(change)="$event ? toggleAllRows() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
Date
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(row) : null"
|
||||
[checked]="selection.isSelected(row)"
|
||||
>
|
||||
</mat-checkbox>
|
||||
{{ row.date }}
|
||||
</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef></mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
@@ -82,8 +112,15 @@
|
||||
|
||||
<!-- Narration Column -->
|
||||
<ng-container matColumnDef="narration">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Narration</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.narration }}</mat-cell>
|
||||
<mat-header-cell *matHeaderCellDef>Narration</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
>{{ row.narration }}
|
||||
<mat-chip-set>
|
||||
@for (tag of row.tags; track tag) {
|
||||
<mat-chip>{{ tag }}</mat-chip>
|
||||
}
|
||||
</mat-chip-set></mat-cell
|
||||
>
|
||||
<mat-footer-cell *matFooterCellDef></mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
@@ -91,21 +128,23 @@
|
||||
<ng-container matColumnDef="debit">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Debit</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.debit | currency: 'INR' | clear }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right">{{ debit | currency: 'INR' }}</mat-footer-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right">{{ dataSource.debit | currency: 'INR' }}</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Credit Column -->
|
||||
<ng-container matColumnDef="credit">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Credit</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.credit | currency: 'INR' | clear }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right">{{ credit | currency: 'INR' }}</mat-footer-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right">{{ dataSource.credit | currency: 'INR' }}</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Running Column -->
|
||||
<ng-container matColumnDef="running">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Running</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.running | currency: 'INR' | accounting }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right">{{ running | currency: 'INR' | accounting }}</mat-footer-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right">{{
|
||||
dataSource.running | currency: 'INR' | accounting
|
||||
}}</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
|
||||
import { MatIconButton, MatButton } from '@angular/material/button';
|
||||
@@ -11,6 +12,7 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortHeader } from '@angular/material/sort';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
@@ -29,7 +31,7 @@ import {
|
||||
} from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
@@ -40,7 +42,13 @@ import { ToCsvService } from '../shared/to-csv.service';
|
||||
|
||||
import { Ledger } from './ledger';
|
||||
import { LedgerDataSource } from './ledger-datasource';
|
||||
import { LedgerService } from './ledger.service';
|
||||
import { MatSelect } from '@angular/material/select';
|
||||
import { MatChip, MatChipSet } from '@angular/material/chips';
|
||||
import { LedgerItem } from './ledger-item';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { Tag } from '../tag/tag';
|
||||
import { TagDialogComponent } from '../tag-dialog/tag-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ledger',
|
||||
@@ -52,6 +60,9 @@ import { LedgerService } from './ledger.service';
|
||||
MatCardHeader,
|
||||
MatCardTitleGroup,
|
||||
MatCardTitle,
|
||||
MatCheckbox,
|
||||
MatChip,
|
||||
MatChipSet,
|
||||
MatIconButton,
|
||||
MatIcon,
|
||||
MatCardContent,
|
||||
@@ -68,6 +79,7 @@ import { LedgerService } from './ledger.service';
|
||||
MatOption,
|
||||
MatButton,
|
||||
MatTable,
|
||||
MatSelect,
|
||||
MatSort,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
@@ -97,17 +109,21 @@ export class LedgerComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
info: Ledger = new Ledger();
|
||||
dataSource: LedgerDataSource = new LedgerDataSource(this.info.body);
|
||||
body = new BehaviorSubject<LedgerItem[]>([]);
|
||||
filter = new BehaviorSubject<string[]>([]);
|
||||
dataSource: LedgerDataSource = new LedgerDataSource(this.body, this.filter);
|
||||
selection = new SelectionModel<LedgerItem>(true, []);
|
||||
|
||||
allTags: Tag[] = [];
|
||||
tags: string[] = [];
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<Date>;
|
||||
finishDate: FormControl<Date>;
|
||||
account: FormControl<string | null>;
|
||||
tags: FormControl<string[]>;
|
||||
}>;
|
||||
|
||||
selectedRowId = '';
|
||||
debit = 0;
|
||||
credit = 0;
|
||||
running = 0;
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['date', 'particulars', 'type', 'narration', 'debit', 'credit', 'running'];
|
||||
|
||||
@@ -123,14 +139,16 @@ export class LedgerComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
private toCsv: ToCsvService,
|
||||
private ser: LedgerService,
|
||||
private accountSer: AccountService,
|
||||
private tagSer: TagService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
finishDate: new FormControl(new Date(), { nonNullable: true }),
|
||||
account: new FormControl<string | null>(null),
|
||||
tags: new FormControl<string[]>([], { nonNullable: true }),
|
||||
});
|
||||
|
||||
this.accounts = this.form.controls.account.valueChanges.pipe(
|
||||
@@ -142,16 +160,29 @@ export class LedgerComponent implements OnInit, AfterViewInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: Ledger };
|
||||
const data = value as { tags: Tag[]; info: Ledger };
|
||||
|
||||
this.allTags = data.tags;
|
||||
this.info = data.info;
|
||||
this.calculateTotals();
|
||||
this.tags = [
|
||||
'(None)',
|
||||
...Array.from(new Set(this.info.body.map((x) => x.tags).reduce((list, tags) => list.concat(tags), []))),
|
||||
];
|
||||
this.form.setValue({
|
||||
account: this.info.account?.name ?? '',
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
|
||||
tags: this.tags,
|
||||
});
|
||||
this.dataSource = new LedgerDataSource(this.info.body, this.paginator, this.sort);
|
||||
this.body.next(this.info.body);
|
||||
this.filter.next(this.tags);
|
||||
if (!this.dataSource.paginator) {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
}
|
||||
if (!this.dataSource.sort) {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
this.selection.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,22 +198,8 @@ export class LedgerComponent implements OnInit, AfterViewInit {
|
||||
return !account ? '' : typeof account === 'string' ? account : account.name;
|
||||
}
|
||||
|
||||
calculateTotals() {
|
||||
this.debit = 0;
|
||||
this.credit = 0;
|
||||
this.running = 0;
|
||||
this.info.body.forEach((item) => {
|
||||
if (item.type !== 'Opening Balance') {
|
||||
this.debit += item.debit;
|
||||
this.credit += item.credit;
|
||||
if (item.posted) {
|
||||
this.running += item.debit - item.credit;
|
||||
}
|
||||
} else {
|
||||
this.running += item.debit - item.credit;
|
||||
}
|
||||
item.running = this.running;
|
||||
});
|
||||
tagChanges(value: string[]) {
|
||||
this.filter.next(value);
|
||||
}
|
||||
|
||||
selected(event: MatAutocompleteSelectedEvent): void {
|
||||
@@ -193,6 +210,46 @@ export class LedgerComponent implements OnInit, AfterViewInit {
|
||||
this.selectedRowId = id;
|
||||
}
|
||||
|
||||
/** Whether the number of selected elements matches the total number of rows. */
|
||||
isAllSelected() {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.dataSource.data.length;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
/** Selects all rows if they are not all selected; otherwise clear selection. */
|
||||
toggleAllRows() {
|
||||
if (this.isAllSelected()) {
|
||||
this.selection.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection.select(...this.dataSource.data);
|
||||
}
|
||||
|
||||
labels() {
|
||||
if (!this.selection.selected.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog
|
||||
.open(TagDialogComponent, {
|
||||
width: '750px',
|
||||
data: {
|
||||
tags: this.allTags,
|
||||
vouchers: this.selection.selected,
|
||||
},
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
if (result) {
|
||||
this.router.navigateByUrl(this.router.url);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
const info = this.getInfo();
|
||||
if (info.account) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authGuard } from '../auth/auth-guard.service';
|
||||
|
||||
import { LedgerComponent } from './ledger.component';
|
||||
import { ledgerResolver } from './ledger.resolver';
|
||||
import { tagListResolver } from '../tag/tag-list.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,6 +15,7 @@ export const routes: Routes = [
|
||||
permission: 'Ledger',
|
||||
},
|
||||
resolve: {
|
||||
tags: tagListResolver,
|
||||
info: ledgerResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
@@ -26,6 +28,7 @@ export const routes: Routes = [
|
||||
permission: 'Ledger',
|
||||
},
|
||||
resolve: {
|
||||
tags: tagListResolver,
|
||||
info: ledgerResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
|
||||
Reference in new issue
Block a user