Feature: Filtering and tagging now available in Ledger
This commit is contained in:
parent
336a8f59c5
commit
c0eca311c7
brewman
alembic/versions
brewman
overlord/src/app
app.routes.ts
core
journal
ledger
payment
purchase-return
purchase
receipt
tag-dialog
tag-dialog.component.csstag-dialog.component.htmltag-dialog.component.spec.tstag-dialog.component.tstag-list.ts
tag
tag-detail
tag-detail.component.csstag-detail.component.htmltag-detail.component.spec.tstag-detail.component.ts
tag-list.resolver.spec.tstag-list.resolver.tstag-list
tag-list-datasource.tstag-list.component.csstag-list.component.htmltag-list.component.spec.tstag-list.component.ts
tag.resolver.spec.tstag.resolver.tstag.routes.tstag.service.spec.tstag.service.tstag.ts@ -0,0 +1,30 @@
|
||||
"""voucher tag permission
|
||||
|
||||
Revision ID: fab52fb911e4
|
||||
Revises: c460289ba94d
|
||||
Create Date: 2024-07-28 16:14:56.020397
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "fab52fb911e4"
|
||||
down_revision = "c460289ba94d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
permissions = sa.table("permissions", sa.column("id", sa.Uuid), sa.column("name", sa.String))
|
||||
op.execute(permissions.insert().values(id="4a43fcf7-c104-4c5d-8f46-7a4b8f997972", name="Tags"))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
permissions = sa.table("permissions", sa.column("id", sa.Uuid), sa.column("name", sa.String))
|
||||
op.execute(permissions.delete().where(permissions.c.id == "4a43fcf7-c104-4c5d-8f46-7a4b8f997972"))
|
||||
# ### end Alembic commands ###
|
@ -114,6 +114,7 @@ def build_report(account_id: uuid.UUID, start_date: str, finish_date: str, db: S
|
||||
debit=debit,
|
||||
credit=credit,
|
||||
posted=voucher.posted,
|
||||
tags=[t.name for t in voucher.tags],
|
||||
)
|
||||
)
|
||||
return body
|
||||
@ -146,4 +147,5 @@ def opening_balance(account_id: uuid.UUID, start_date: str, db: Session) -> sche
|
||||
debit=debit,
|
||||
credit=credit,
|
||||
posted=True,
|
||||
tags=["Opening Balance"],
|
||||
)
|
||||
|
@ -2,6 +2,7 @@ import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Security, status
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@ -14,6 +15,7 @@ from brewman.models.voucher_tag import VoucherTag
|
||||
from ..core.security import get_current_active_user as get_user
|
||||
from ..db.session import SessionFuture
|
||||
from ..models.tag import Tag
|
||||
from ..schemas.tag_vouchers import TagVouchers
|
||||
from ..schemas.user import UserToken
|
||||
|
||||
|
||||
@ -118,6 +120,38 @@ def show_id(
|
||||
return tag_info(item)
|
||||
|
||||
|
||||
@router.post("/vouchers", response_model=bool)
|
||||
def tag_vouchers(
|
||||
data: TagVouchers,
|
||||
user: UserToken = Security(get_user, scopes=["tags"]),
|
||||
) -> bool:
|
||||
try:
|
||||
with SessionFuture() as db:
|
||||
old_tags = db.execute(select(Tag.name)).scalars().all()
|
||||
new_tag = next((t.name for t in data.tags if t.name not in old_tags), None)
|
||||
if new_tag is not None and new_tag != "":
|
||||
db.add(Tag(name=new_tag))
|
||||
db.flush()
|
||||
remove = [t.name for t in data.tags if t.enabled is False]
|
||||
tags = db.execute(select(Tag.id).where(Tag.name.in_(remove))).scalars().all()
|
||||
db.execute(
|
||||
delete(VoucherTag).where(VoucherTag.tag_id.in_(tags), VoucherTag.voucher_id.in_(data.voucher_ids))
|
||||
)
|
||||
|
||||
additional = [t.name for t in data.tags if t.enabled is True]
|
||||
tags = db.execute(select(Tag.id).where(Tag.name.in_(additional))).scalars().all()
|
||||
for vid in data.voucher_ids:
|
||||
for t in tags:
|
||||
db.execute(pg_insert(VoucherTag).values(voucher_id=vid, tag_id=t).on_conflict_do_nothing())
|
||||
db.commit()
|
||||
return True
|
||||
except SQLAlchemyError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
def tag_info(item: Tag) -> schemas.Tag:
|
||||
return schemas.Tag(
|
||||
id_=item.id,
|
||||
|
@ -24,6 +24,7 @@ class LedgerItem(BaseModel):
|
||||
debit: Daf
|
||||
credit: Daf
|
||||
posted: bool
|
||||
tags: list[str]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("date_", mode="before")
|
||||
|
17
brewman/brewman/schemas/tag_vouchers.py
Normal file
17
brewman/brewman/schemas/tag_vouchers.py
Normal file
@ -0,0 +1,17 @@
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from brewman.schemas import to_camel
|
||||
|
||||
|
||||
class TagVouchersItem(BaseModel):
|
||||
name: str
|
||||
enabled: bool
|
||||
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
|
||||
|
||||
|
||||
class TagVouchers(BaseModel):
|
||||
tags: list[TagVouchersItem]
|
||||
voucher_ids: list[uuid.UUID]
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
@ -153,6 +153,10 @@ export const routes: Routes = [
|
||||
path: 'stock-movement',
|
||||
loadChildren: () => import('./stock-movement/stock-movement.routes').then((mod) => mod.routes),
|
||||
},
|
||||
{
|
||||
path: 'tags',
|
||||
loadChildren: () => import('./tag/tag.routes').then((mod) => mod.routes),
|
||||
},
|
||||
{
|
||||
path: 'trial-balance',
|
||||
loadChildren: () => import('./trial-balance/trial-balance.routes').then((mod) => mod.routes),
|
||||
|
@ -56,6 +56,7 @@
|
||||
<a mat-menu-item routerLink="/recipes">Recipes</a>
|
||||
<a mat-menu-item routerLink="/recipe-templates">Recipe Templates</a>
|
||||
<a mat-menu-item routerLink="/periods">Periods</a>
|
||||
<a mat-menu-item routerLink="/tags">Tags</a>
|
||||
</mat-menu>
|
||||
<button mat-button [matMenuTriggerFor]="masterMenu">Masters</button>
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Tag } from '../tag/tag';
|
||||
import { Account } from './account';
|
||||
import { CostCentre } from './cost-centre';
|
||||
import { DbFile } from './db-file';
|
||||
@ -5,7 +6,6 @@ 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 {
|
||||
|
@ -45,8 +45,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 { Tag } from '../tag/tag';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
|
@ -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',
|
||||
|
@ -45,8 +45,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 { Tag } from '../tag/tag';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
|
@ -46,8 +46,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 { Tag } from '../tag/tag';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
|
@ -47,8 +47,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 { Tag } from '../tag/tag';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
|
@ -45,8 +45,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 { Tag } from '../tag/tag';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { User } from '../core/user';
|
||||
import { Voucher } from '../core/voucher';
|
||||
|
0
overlord/src/app/tag-dialog/tag-dialog.component.css
Normal file
0
overlord/src/app/tag-dialog/tag-dialog.component.css
Normal file
26
overlord/src/app/tag-dialog/tag-dialog.component.html
Normal file
26
overlord/src/app/tag-dialog/tag-dialog.component.html
Normal file
@ -0,0 +1,26 @@
|
||||
<h1 mat-dialog-title>Choose Tags</h1>
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="form" class="flex flex-col">
|
||||
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Create New</mat-label>
|
||||
<input type="text" matInput formControlName="name" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@for (t of tags; track t) {
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-checkbox
|
||||
class="flex-auto"
|
||||
[indeterminate]="t.indeterminate"
|
||||
[checked]="t.checked"
|
||||
(change)="update($event.checked, t)"
|
||||
>{{ t.name }}</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-raised-button color="warn" [mat-dialog-close]="false" cdkFocusInitial class="mr-5">Cancel</button>
|
||||
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
</div>
|
22
overlord/src/app/tag-dialog/tag-dialog.component.spec.ts
Normal file
22
overlord/src/app/tag-dialog/tag-dialog.component.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagDialogComponent } from './tag-dialog.component';
|
||||
|
||||
describe('TagDialogComponent', () => {
|
||||
let component: TagDialogComponent;
|
||||
let fixture: ComponentFixture<TagDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TagDialogComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TagDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
119
overlord/src/app/tag-dialog/tag-dialog.component.ts
Normal file
119
overlord/src/app/tag-dialog/tag-dialog.component.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatOption } from '@angular/material/core';
|
||||
import { MatDatepicker } from '@angular/material/datepicker';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormField, MatLabel, MatHint, MatPrefix } from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { MatSelect } from '@angular/material/select';
|
||||
|
||||
import { Tag } from '../tag/tag';
|
||||
import { AccountingPipe } from '../shared/accounting.pipe';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { LedgerItem } from '../ledger/ledger-item';
|
||||
import { TagList } from './tag-list';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-dialog',
|
||||
templateUrl: './tag-dialog.component.html',
|
||||
styleUrls: ['./tag-dialog.component.css'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AccountingPipe,
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
CdkScrollable,
|
||||
MatCheckbox,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
ReactiveFormsModule,
|
||||
MatFormField,
|
||||
MatSelect,
|
||||
MatDatepicker,
|
||||
MatOption,
|
||||
MatLabel,
|
||||
MatInput,
|
||||
MatAutocompleteTrigger,
|
||||
MatHint,
|
||||
MatAutocomplete,
|
||||
MatPrefix,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatDialogClose,
|
||||
],
|
||||
})
|
||||
export class TagDialogComponent implements OnInit {
|
||||
tags: TagList[] = [];
|
||||
form: FormGroup<{
|
||||
name: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<TagDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { tags: Tag[]; vouchers: LedgerItem[] },
|
||||
|
||||
private snackBar: MatSnackBar,
|
||||
private ser: TagService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl(''),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form.patchValue({
|
||||
name: null,
|
||||
});
|
||||
console.log(this.data);
|
||||
|
||||
this.tags = this.data.tags.map((x) => {
|
||||
const v = this.data.vouchers.filter((z) => z.tags.indexOf(x.name) > -1).length;
|
||||
|
||||
switch (true) {
|
||||
case v === 0:
|
||||
return new TagList({ id: x.id, name: x.name, checked: false, indeterminate: false });
|
||||
case v === this.data.vouchers.length:
|
||||
return new TagList({ id: x.id, name: x.name, checked: true, indeterminate: false });
|
||||
default:
|
||||
return new TagList({ id: x.id, name: x.name, checked: false, indeterminate: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(checked: boolean, tag: TagList) {
|
||||
tag.indeterminate = false;
|
||||
tag.checked = checked;
|
||||
console.log(this.tags);
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
const tags = this.tags.filter((x) => !x.indeterminate).map((x) => ({ name: x.name, enabled: x.checked }));
|
||||
const name = this.form.value.name;
|
||||
if (name) {
|
||||
tags.push({ name: name, enabled: true });
|
||||
}
|
||||
const vouchers = this.data.vouchers.map((x) => x.id);
|
||||
this.ser.vouchers(tags, vouchers).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
14
overlord/src/app/tag-dialog/tag-list.ts
Normal file
14
overlord/src/app/tag-dialog/tag-list.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export class TagList {
|
||||
id: string | null;
|
||||
name: string;
|
||||
indeterminate: boolean;
|
||||
checked: boolean;
|
||||
|
||||
public constructor(init?: Partial<TagList>) {
|
||||
this.id = null;
|
||||
this.name = '';
|
||||
this.indeterminate = false;
|
||||
this.checked = false;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
0
overlord/src/app/tag/tag-detail/tag-detail.component.css
Normal file
0
overlord/src/app/tag/tag-detail/tag-detail.component.css
Normal file
21
overlord/src/app/tag/tag-detail/tag-detail.component.html
Normal file
21
overlord/src/app/tag/tag-detail/tag-detail.component.html
Normal file
@ -0,0 +1,21 @@
|
||||
<mat-card class="lg:max-w-[50%]">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Tag</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" class="flex flex-col">
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" (keyup.enter)="save()" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button (click)="save()" color="primary" class="mr-5">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
22
overlord/src/app/tag/tag-detail/tag-detail.component.spec.ts
Normal file
22
overlord/src/app/tag/tag-detail/tag-detail.component.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagDetailComponent } from './tag-detail.component';
|
||||
|
||||
describe('TagDetailComponent', () => {
|
||||
let component: TagDetailComponent;
|
||||
let fixture: ComponentFixture<TagDetailComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TagDetailComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TagDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
116
overlord/src/app/tag/tag-detail/tag-detail.component.ts
Normal file
116
overlord/src/app/tag/tag-detail/tag-detail.component.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatCard, MatCardHeader, MatCardTitle, MatCardContent, MatCardActions } from '@angular/material/card';
|
||||
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Tag } from '../tag';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TagService } from '../tag.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from 'src/app/shared/confirm-dialog/confirm-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-detail',
|
||||
templateUrl: './tag-detail.component.html',
|
||||
styleUrls: ['./tag-detail.component.css'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCard,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
MatCardContent,
|
||||
ReactiveFormsModule,
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
MatInput,
|
||||
MatCardActions,
|
||||
MatButton,
|
||||
],
|
||||
})
|
||||
export class TagDetailComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string | null>;
|
||||
}>;
|
||||
|
||||
item: Tag = new Tag();
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private dialog: MatDialog,
|
||||
private ser: TagService,
|
||||
) {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string | null>(null),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: Tag };
|
||||
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(item: Tag) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
name: this.item.name,
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.nameElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.ser.saveOrUpdate(this.getItem()).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/tags');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/tags');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
confirmDelete(): void {
|
||||
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||
width: '250px',
|
||||
data: { title: 'Delete Tag?', content: 'Are you sure? This cannot be undone.' },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
this.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getItem(): Tag {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
return this.item;
|
||||
}
|
||||
}
|
19
overlord/src/app/tag/tag-list.resolver.spec.ts
Normal file
19
overlord/src/app/tag/tag-list.resolver.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Tag } from '../tag/tag';
|
||||
|
||||
import { tagListResolver } from './tag-list.resolver';
|
||||
|
||||
describe('tagListResolver', () => {
|
||||
const executeResolver: ResolveFn<Tag[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => tagListResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
10
overlord/src/app/tag/tag-list.resolver.ts
Normal file
10
overlord/src/app/tag/tag-list.resolver.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Tag } from './tag';
|
||||
|
||||
import { TagService } from './tag.service';
|
||||
|
||||
export const tagListResolver: ResolveFn<Tag[]> = () => {
|
||||
return inject(TagService).list();
|
||||
};
|
69
overlord/src/app/tag/tag-list/tag-list-datasource.ts
Normal file
69
overlord/src/app/tag/tag-list/tag-list-datasource.ts
Normal file
@ -0,0 +1,69 @@
|
||||
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 { Tag } from '../tag';
|
||||
|
||||
/** 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 TagListDataSource extends DataSource<Tag> {
|
||||
constructor(
|
||||
public data: Tag[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Tag[]> {
|
||||
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
|
||||
if (this.paginator) {
|
||||
dataMutations.push((this.paginator as MatPaginator).page);
|
||||
}
|
||||
if (this.sort) {
|
||||
dataMutations.push((this.sort as MatSort).sortChange);
|
||||
}
|
||||
|
||||
// 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]))),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getPagedData(data: Tag[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: Tag[]) {
|
||||
if (this.sort === undefined) {
|
||||
return data;
|
||||
}
|
||||
if (!this.sort.active || this.sort.direction === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
const sort = this.sort as MatSort;
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
switch (sort.active) {
|
||||
case 'name':
|
||||
return compare(a.name, b.name, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
0
overlord/src/app/tag/tag-list/tag-list.component.css
Normal file
0
overlord/src/app/tag/tag-list/tag-list.component.css
Normal file
34
overlord/src/app/tag/tag-list/tag-list.component.html
Normal file
34
overlord/src/app/tag/tag-list/tag-list.component.html
Normal file
@ -0,0 +1,34 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title-group>
|
||||
<mat-card-title>Tags</mat-card-title>
|
||||
<a mat-button [routerLink]="['/tags', 'new']">
|
||||
<mat-icon>add_box</mat-icon>
|
||||
Add
|
||||
</a>
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
><a [routerLink]="['/tags', row.id]">{{ row.name }}</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator
|
||||
#paginator
|
||||
[length]="dataSource.data.length"
|
||||
[pageIndex]="0"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250]"
|
||||
>
|
||||
</mat-paginator>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
22
overlord/src/app/tag/tag-list/tag-list.component.spec.ts
Normal file
22
overlord/src/app/tag/tag-list/tag-list.component.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagListComponent } from './tag-list.component';
|
||||
|
||||
describe('TagListComponent', () => {
|
||||
let component: TagListComponent;
|
||||
let fixture: ComponentFixture<TagListComponent>;
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule, TagListComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TagListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should compile', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
72
overlord/src/app/tag/tag-list/tag-list.component.ts
Normal file
72
overlord/src/app/tag/tag-list/tag-list.component.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatAnchor } from '@angular/material/button';
|
||||
import { MatCard, MatCardHeader, MatCardTitleGroup, MatCardTitle, MatCardContent } from '@angular/material/card';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortHeader } from '@angular/material/sort';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
} from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
import { Tag } from '../tag';
|
||||
|
||||
import { TagListDataSource } from './tag-list-datasource';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-list',
|
||||
templateUrl: './tag-list.component.html',
|
||||
styleUrls: ['./tag-list.component.css'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCard,
|
||||
MatCardHeader,
|
||||
MatCardTitleGroup,
|
||||
MatCardTitle,
|
||||
MatAnchor,
|
||||
RouterLink,
|
||||
MatIcon,
|
||||
MatCardContent,
|
||||
MatTable,
|
||||
MatSort,
|
||||
MatColumnDef,
|
||||
MatHeaderCellDef,
|
||||
MatHeaderCell,
|
||||
MatSortHeader,
|
||||
MatCellDef,
|
||||
MatCell,
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatPaginator,
|
||||
],
|
||||
})
|
||||
export class TagListComponent implements OnInit {
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
list: Tag[] = [];
|
||||
dataSource: TagListDataSource = new TagListDataSource(this.list, this.paginator, this.sort);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name'];
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Tag[] };
|
||||
|
||||
this.list = data.list;
|
||||
});
|
||||
this.dataSource = new TagListDataSource(this.list, this.paginator, this.sort);
|
||||
}
|
||||
}
|
19
overlord/src/app/tag/tag.resolver.spec.ts
Normal file
19
overlord/src/app/tag/tag.resolver.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Tag } from '../tag/tag';
|
||||
|
||||
import { tagResolver } from './tag.resolver';
|
||||
|
||||
describe('tagResolver', () => {
|
||||
const executeResolver: ResolveFn<Tag> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => tagResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
11
overlord/src/app/tag/tag.resolver.ts
Normal file
11
overlord/src/app/tag/tag.resolver.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Tag } from './tag';
|
||||
|
||||
import { TagService } from './tag.service';
|
||||
|
||||
export const tagResolver: ResolveFn<Tag> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(TagService).get(id);
|
||||
};
|
44
overlord/src/app/tag/tag.routes.ts
Normal file
44
overlord/src/app/tag/tag.routes.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
|
||||
import { TagDetailComponent } from './tag-detail/tag-detail.component';
|
||||
import { TagListComponent } from './tag-list/tag-list.component';
|
||||
import { tagListResolver } from './tag-list.resolver';
|
||||
import { tagResolver } from './tag.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: TagListComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
permission: 'Tags',
|
||||
},
|
||||
resolve: {
|
||||
list: tagListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
component: TagDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
permission: 'Tags',
|
||||
},
|
||||
resolve: {
|
||||
item: tagResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: TagDetailComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
permission: 'Tags',
|
||||
},
|
||||
resolve: {
|
||||
item: tagResolver,
|
||||
},
|
||||
},
|
||||
];
|
17
overlord/src/app/tag/tag.service.spec.ts
Normal file
17
overlord/src/app/tag/tag.service.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TagService } from './tag.service';
|
||||
|
||||
describe('TagService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [TagService, provideHttpClient(withInterceptorsFromDi())],
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([TagService], (service: TagService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -3,8 +3,8 @@ 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';
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
|
||||
const url = '/api/tags';
|
||||
const serviceName = 'TagService';
|
||||
@ -18,16 +18,17 @@ export class TagService {
|
||||
private log: ErrorLoggerService,
|
||||
) {}
|
||||
|
||||
get(id: string): Observable<Tag> {
|
||||
get(id: string | null): Observable<Tag> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Tag>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Get Tag'))) as Observable<Tag>;
|
||||
.get<Tag>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
list(): Observable<Tag[]> {
|
||||
return this.http
|
||||
.get<Tag[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'List Tag'))) as Observable<Tag[]>;
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Tag[]>;
|
||||
}
|
||||
|
||||
autocomplete(query: string): Observable<Tag[]> {
|
||||
@ -37,10 +38,16 @@ export class TagService {
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Tag[]>;
|
||||
}
|
||||
|
||||
delete(id: string): Observable<Tag> {
|
||||
save(tag: Tag): Observable<Tag> {
|
||||
return this.http
|
||||
.delete<Tag>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Delete Tag'))) as Observable<Tag>;
|
||||
.post<Tag>(`${url}`, tag)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
update(tag: Tag): Observable<Tag> {
|
||||
return this.http
|
||||
.put<Tag>(`${url}/${tag.id}`, tag)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
saveOrUpdate(tag: Tag): Observable<Tag> {
|
||||
@ -50,15 +57,15 @@ export class TagService {
|
||||
return this.update(tag);
|
||||
}
|
||||
|
||||
save(tag: Tag): Observable<Tag> {
|
||||
delete(id: string): Observable<Tag> {
|
||||
return this.http
|
||||
.post<Tag>(`${url}`, tag)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Save Tag'))) as Observable<Tag>;
|
||||
.delete<Tag>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Tag>;
|
||||
}
|
||||
|
||||
update(tag: Tag): Observable<Tag> {
|
||||
vouchers(tags: { name: string; enabled: boolean }[], voucherIds: string[]): Observable<boolean> {
|
||||
return this.http
|
||||
.put<Tag>(`${url}/${tag.id}`, tag)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Update Tag'))) as Observable<Tag>;
|
||||
.post<boolean>(`${url}/vouchers`, { tags: tags, voucherIds: voucherIds })
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Set Voucher Tags'))) as Observable<boolean>;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user