Feature: Filtering and tagging now available in Ledger

This commit is contained in:
tanshu committed 2024-07-28 17:32:16 +05:30
1 parent 336a8f59c5
commit c0eca311c7
40 files changed
+973 -72

No files matched your search

+60 -13
View File
@@ -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;
+2
View File
@@ -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);
}
}
+46 -7
View File
@@ -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>
+84 -27
View File
@@ -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) {
+3
View File
@@ -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',