update accounting journal entry UI and API response
This commit is contained in:
@@ -106,6 +106,40 @@ export class AccountingService {
|
||||
return this.http.get('/journalentries', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* CBS GL Account Ledger Report: returns opening balance, day-wise summary, totals, and paginated entries.
|
||||
*/
|
||||
getGLAccountLedger(params: {
|
||||
glAccountId?: number;
|
||||
officeId?: number;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
dateFormat?: string;
|
||||
locale?: string;
|
||||
manualEntriesOnly?: boolean;
|
||||
transactionId?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: string;
|
||||
sortOrder?: string;
|
||||
}): Observable<any> {
|
||||
let httpParams = new HttpParams();
|
||||
if (params.glAccountId) httpParams = httpParams.set('glAccountId', params.glAccountId.toString());
|
||||
if (params.officeId) httpParams = httpParams.set('officeId', params.officeId.toString());
|
||||
if (params.fromDate) httpParams = httpParams.set('fromDate', params.fromDate);
|
||||
if (params.toDate) httpParams = httpParams.set('toDate', params.toDate);
|
||||
if (params.dateFormat) httpParams = httpParams.set('dateFormat', params.dateFormat);
|
||||
if (params.locale) httpParams = httpParams.set('locale', params.locale);
|
||||
if (params.manualEntriesOnly != null)
|
||||
httpParams = httpParams.set('manualEntriesOnly', params.manualEntriesOnly.toString());
|
||||
if (params.transactionId) httpParams = httpParams.set('transactionId', params.transactionId);
|
||||
if (params.offset != null) httpParams = httpParams.set('offset', params.offset.toString());
|
||||
if (params.limit != null) httpParams = httpParams.set('limit', params.limit.toString());
|
||||
if (params.orderBy) httpParams = httpParams.set('orderBy', params.orderBy);
|
||||
if (params.sortOrder) httpParams = httpParams.set('sortOrder', params.sortOrder);
|
||||
return this.http.get('/journalentries/ledger', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} transactionId Transaction ID to be reverted.
|
||||
* @param {string} comments Any comments.
|
||||
|
||||
@@ -6,68 +6,74 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
|
||||
/** Custom Services */
|
||||
import { AccountingService } from '../accounting.service';
|
||||
|
||||
/**
|
||||
* Journal entries custom data source to implement server side filtering, pagination and sorting.
|
||||
*/
|
||||
export class JournalEntriesDataSource implements DataSource<any> {
|
||||
/** Journal entries behavior subject to represent loaded journal entries page. */
|
||||
private journalEntriesSubject = new BehaviorSubject<any[]>([]);
|
||||
/** Records subject to represent total number of filtered journal entry records. */
|
||||
private filteredSubject = new BehaviorSubject<any[]>([]);
|
||||
private recordsSubject = new BehaviorSubject<number>(0);
|
||||
/** Records observable which can be subscribed to get the value of total number of filtered journal entry records. */
|
||||
public records$ = this.recordsSubject.asObservable();
|
||||
|
||||
/**
|
||||
* @param {AccountingService} accountingService Accounting Service
|
||||
*/
|
||||
public records$ = this.recordsSubject.asObservable();
|
||||
public entries$ = this.filteredSubject.asObservable();
|
||||
|
||||
private accountTypeFilter = '';
|
||||
|
||||
constructor(private accountingService: AccountingService) {}
|
||||
|
||||
/**
|
||||
* Gets journal entries on the basis of provided parameters and emits the value.
|
||||
* @param {any} filterBy Properties by which entries should be filtered.
|
||||
* @param {string} orderBy Property by which entries should be sorted.
|
||||
* @param {string} sortOrder Sort order: ascending or descending.
|
||||
* @param {number} pageIndex Page number.
|
||||
* @param {number} limit Number of entries within the page.
|
||||
*/
|
||||
getJournalEntries(
|
||||
filterBy: any,
|
||||
orderBy: string = '',
|
||||
sortOrder: string = '',
|
||||
pageIndex: number = 0,
|
||||
limit: number = 10
|
||||
) {
|
||||
getJournalEntries(filterBy: any, orderBy = '', sortOrder = '', pageIndex = 0, limit = 10) {
|
||||
this.journalEntriesSubject.next([]);
|
||||
this.filteredSubject.next([]);
|
||||
|
||||
// Extract client-side glAccountClassification filter — not sent to server
|
||||
const classificationFilter = filterBy.find((f: any) => f.type === 'glAccountClassification');
|
||||
this.accountTypeFilter = classificationFilter?.value || '';
|
||||
const serverFilters = filterBy.filter((f: any) => f.type !== 'glAccountClassification');
|
||||
|
||||
orderBy = orderBy === 'debit' || orderBy === 'credit' ? 'amount' : orderBy;
|
||||
|
||||
// Always request running balance and transaction details for the entries table
|
||||
const runningBalanceFilter = { type: 'runningBalance', value: 'true' };
|
||||
const txnDetailsFilter = { type: 'transactionDetails', value: 'true' };
|
||||
const filtersWithExtras = [
|
||||
...serverFilters,
|
||||
runningBalanceFilter,
|
||||
txnDetailsFilter
|
||||
];
|
||||
|
||||
this.accountingService
|
||||
.getJournalEntries(filterBy, orderBy, sortOrder, pageIndex * limit, limit)
|
||||
.getJournalEntries(filtersWithExtras, orderBy, sortOrder, pageIndex * limit, limit)
|
||||
.subscribe((journalEntries: any) => {
|
||||
this.recordsSubject.next(journalEntries.totalFilteredRecords);
|
||||
this.journalEntriesSubject.next(journalEntries.pageItems);
|
||||
const all: any[] = journalEntries.pageItems || [];
|
||||
this.journalEntriesSubject.next(all);
|
||||
|
||||
// Client-side account type filter
|
||||
const filtered = this.accountTypeFilter
|
||||
? all.filter((e) => e.glAccountType?.value === this.accountTypeFilter)
|
||||
: all;
|
||||
|
||||
this.recordsSubject.next(this.accountTypeFilter ? filtered.length : journalEntries.totalFilteredRecords);
|
||||
this.filteredSubject.next(filtered);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CollectionViewer} collectionViewer
|
||||
*/
|
||||
/** Apply account type filter on already-loaded data without re-fetching */
|
||||
applyAccountTypeFilter(type: string) {
|
||||
this.accountTypeFilter = type;
|
||||
const all = this.journalEntriesSubject.getValue();
|
||||
const filtered = type ? all.filter((e) => e.glAccountType?.value === type) : all;
|
||||
this.recordsSubject.next(filtered.length || (type ? 0 : this.recordsSubject.getValue()));
|
||||
this.filteredSubject.next(filtered);
|
||||
}
|
||||
|
||||
connect(collectionViewer: CollectionViewer): Observable<any[]> {
|
||||
return this.journalEntriesSubject.asObservable();
|
||||
return this.filteredSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CollectionViewer} collectionViewer
|
||||
*/
|
||||
disconnect(collectionViewer: CollectionViewer): void {
|
||||
this.journalEntriesSubject.complete();
|
||||
this.filteredSubject.complete();
|
||||
this.recordsSubject.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,179 +6,715 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container layout-row-wrap gap-2px responsive-column filter-container">
|
||||
<mat-form-field class="flex-31">
|
||||
<mat-label>{{ 'labels.inputs.Office Name' | translate }}</mat-label>
|
||||
<input matInput [formControl]="officeName" [matAutocomplete]="officeNameAutocomplete" />
|
||||
</mat-form-field>
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
HERO HEADER — glassmorphism + animated gradient
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-hero">
|
||||
<div class="gl-hero__bg"></div>
|
||||
<div class="gl-hero__content">
|
||||
<div class="gl-hero__left">
|
||||
<div class="gl-hero__icon-wrap">
|
||||
<fa-icon icon="building-columns" class="gl-hero__icon"></fa-icon>
|
||||
<div class="gl-hero__icon-pulse"></div>
|
||||
</div>
|
||||
<div class="gl-hero__text">
|
||||
<div class="gl-hero__breadcrumb">
|
||||
<fa-icon icon="home" class="gl-hero__bc-icon"></fa-icon>
|
||||
<span>Accounting</span>
|
||||
<fa-icon icon="chevron-right" class="gl-hero__bc-sep"></fa-icon>
|
||||
<span class="gl-hero__bc-active">General Ledger</span>
|
||||
</div>
|
||||
<h1 class="gl-hero__title">General Ledger</h1>
|
||||
<p class="gl-hero__subtitle" *ngIf="!glAccountInfo">
|
||||
Full double-entry journal — view, filter & analyse all accounting entries
|
||||
</p>
|
||||
<div class="gl-hero__acct-pill" *ngIf="glAccountInfo">
|
||||
<span [class]="'gl-pill gl-pill--' + (glAccountInfo.type?.value | lowercase)">
|
||||
<fa-icon [icon]="getAccountTypeIcon(glAccountInfo.type?.value)" class="gl-pill__icon"></fa-icon>
|
||||
{{ glAccountInfo.type?.value }}
|
||||
</span>
|
||||
<span class="gl-hero__acct-code">{{ glAccountInfo.glCode }}</span>
|
||||
<span class="gl-hero__acct-sep">·</span>
|
||||
<span class="gl-hero__acct-name">{{ glAccountInfo.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mifosx-gl-account-selector
|
||||
class="flex-31"
|
||||
[inputFormControl]="glAccount"
|
||||
[glAccountList]="glAccountData"
|
||||
[required]="true"
|
||||
[inputLabel]="'GL Account Name or Code'"
|
||||
>
|
||||
</mifosx-gl-account-selector>
|
||||
|
||||
<mat-form-field class="flex-31">
|
||||
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
|
||||
<mat-select [formControl]="entryTypeFilter" (selectionChange)="applyFilter($event.value, 'manualEntriesOnly')">
|
||||
@for (filter of entryTypeFilterData; track filter) {
|
||||
<mat-option [value]="filter.value">
|
||||
{{ filter.option | translate }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-31" (click)="transactionDateFromPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Date From' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="transactionDateFromPicker"
|
||||
[formControl]="transactionDateFrom"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="transactionDateFromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #transactionDateFromPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-31" (click)="transactionDateToPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Date To' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="transactionDateToPicker"
|
||||
[formControl]="transactionDateTo"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="transactionDateToPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #transactionDateToPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-31">
|
||||
<mat-label>{{ 'labels.inputs.Transaction ID' | translate }}</mat-label>
|
||||
<input matInput [formControl]="transactionId" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-31" (click)="submittedOnDateFromPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Submitted on Date From' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[max]="submittedOnDateTo.value"
|
||||
[matDatepicker]="submittedOnDateFromPicker"
|
||||
[formControl]="submittedOnDateFrom"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="submittedOnDateFromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #submittedOnDateFromPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-31" (click)="submittedOnDateToPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Submitted on Date To' | translate }} </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[min]="submittedOnDateFrom.value"
|
||||
[matDatepicker]="submittedOnDateToPicker"
|
||||
[formControl]="submittedOnDateTo"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="submittedOnDateToPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #submittedOnDateToPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<div class="gl-hero__actions">
|
||||
<div class="gl-hero__meta" *ngIf="ledgerData?.fromDate || ledgerData?.toDate">
|
||||
<fa-icon icon="calendar-alt" class="gl-hero__meta-icon"></fa-icon>
|
||||
<span>{{ formatLedgerDate(ledgerData?.fromDate) }} → {{ formatLedgerDate(ledgerData?.toDate) }}</span>
|
||||
</div>
|
||||
<button
|
||||
class="gl-hero-btn gl-hero-btn--ghost"
|
||||
(click)="loadLedgerSummary(); loadJournalEntriesPage()"
|
||||
matTooltip="Refresh data"
|
||||
>
|
||||
<fa-icon icon="sync" [class.fa-spin]="ledgerLoading"></fa-icon>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="gl-hero-btn gl-hero-btn--primary" [routerLink]="['/accounting/journal-entries/create']">
|
||||
<fa-icon icon="plus"></fa-icon>
|
||||
New Entry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Autocomplete data -->
|
||||
<mat-autocomplete autoActiveFirstOption #officeNameAutocomplete="matAutocomplete" [displayWith]="displayOfficeName">
|
||||
@for (office of filteredOfficeData | async; track office) {
|
||||
<mat-option [value]="{ id: office.id, name: office.name }">
|
||||
{{ office.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
|
||||
<div class="mat-elevation-z8 container table-container">
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Entry ID' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.id }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="officeName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Office' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.officeName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactionId">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Transaction ID' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.transactionId }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactionDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Transaction Date' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.transactionDate | dateFormat }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="glAccountType">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.glAccountType.value }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="createdByUserName">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Created By' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.createdByUserName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="submittedOnDate">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Submitted on date' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.submittedOnDate | datetimeFormat }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="glAccountCode">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Account Code' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.glAccountCode }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="glAccountName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Account Name' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.glAccountName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="currency">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Currency' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">{{ journalEntry.currency.code }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="debit">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Debit' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">
|
||||
@if (journalEntry.entryType.value === 'DEBIT') {
|
||||
<span> {{ journalEntry.amount | formatNumber }} </span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="credit">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Credit' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let journalEntry">
|
||||
@if (journalEntry.entryType.value === 'CREDIT') {
|
||||
<span> {{ journalEntry.amount | formatNumber }} </span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[routerLink]="['transactions/view', row.transactionId]"
|
||||
class="select-row"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator
|
||||
[length]="dataSource?.records$ | async"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[10, 25, 50, 100]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
LOADING SKELETON / PROGRESS BAR
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-progress-bar" *ngIf="ledgerLoading">
|
||||
<div class="gl-progress-bar__fill"></div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
GL ACCOUNT DETAIL STRIP (visible when account selected)
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-acct-strip" *ngIf="glAccountInfo">
|
||||
<div class="gl-acct-strip__item">
|
||||
<div class="gl-acct-strip__label">Account Code</div>
|
||||
<div class="gl-acct-strip__value gl-mono">{{ glAccountInfo.glCode }}</div>
|
||||
</div>
|
||||
<div class="gl-acct-strip__divider"></div>
|
||||
<div class="gl-acct-strip__item">
|
||||
<div class="gl-acct-strip__label">Account Name</div>
|
||||
<div class="gl-acct-strip__value">{{ glAccountInfo.name }}</div>
|
||||
</div>
|
||||
<div class="gl-acct-strip__divider"></div>
|
||||
<div class="gl-acct-strip__item">
|
||||
<div class="gl-acct-strip__label">Classification</div>
|
||||
<div class="gl-acct-strip__value">
|
||||
<span [class]="'gl-pill gl-pill--sm gl-pill--' + (glAccountInfo.type?.value | lowercase)">
|
||||
<fa-icon [icon]="getAccountTypeIcon(glAccountInfo.type?.value)" class="gl-pill__icon"></fa-icon>
|
||||
{{ glAccountInfo.type?.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-acct-strip__divider"></div>
|
||||
<div class="gl-acct-strip__item">
|
||||
<div class="gl-acct-strip__label">Usage</div>
|
||||
<div class="gl-acct-strip__value">{{ glAccountInfo.usage?.value || '—' }}</div>
|
||||
</div>
|
||||
<div class="gl-acct-strip__divider"></div>
|
||||
<div class="gl-acct-strip__item">
|
||||
<div class="gl-acct-strip__label">Manual Entries</div>
|
||||
<div class="gl-acct-strip__value">
|
||||
<span [class]="glAccountInfo.manualEntriesAllowed ? 'gl-status gl-status--on' : 'gl-status gl-status--off'">
|
||||
<fa-icon [icon]="glAccountInfo.manualEntriesAllowed ? 'check-circle' : 'times-circle'"></fa-icon>
|
||||
{{ glAccountInfo.manualEntriesAllowed ? 'Allowed' : 'Restricted' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-acct-strip__divider" *ngIf="glAccountInfo.description"></div>
|
||||
<div class="gl-acct-strip__item gl-acct-strip__item--desc" *ngIf="glAccountInfo.description">
|
||||
<div class="gl-acct-strip__label">Description</div>
|
||||
<div class="gl-acct-strip__value gl-acct-strip__value--muted">{{ glAccountInfo.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
KPI CARDS — 6 metrics with micro bar indicators
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-kpi-grid" [class.gl-kpi-grid--loading]="ledgerLoading">
|
||||
<!-- Opening Balance -->
|
||||
<div class="gl-kpi gl-kpi--opening">
|
||||
<div class="gl-kpi__header">
|
||||
<div class="gl-kpi__icon-box">
|
||||
<fa-icon icon="door-open"></fa-icon>
|
||||
</div>
|
||||
<div class="gl-kpi__trend gl-kpi__trend--neutral" matTooltip="Balance before the selected period">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__body">
|
||||
<div class="gl-kpi__label">Opening Balance</div>
|
||||
<div class="gl-kpi__value" [class.gl-kpi__value--neg]="openingBalance < 0">
|
||||
{{ openingBalance | formatNumber }}
|
||||
</div>
|
||||
<div class="gl-kpi__caption">Before {{ formatLedgerDate(ledgerData?.fromDate) || 'period start' }}</div>
|
||||
</div>
|
||||
<div class="gl-kpi__bar gl-kpi__bar--opening"></div>
|
||||
</div>
|
||||
|
||||
<!-- Total Debit -->
|
||||
<div class="gl-kpi gl-kpi--dr">
|
||||
<div class="gl-kpi__header">
|
||||
<div class="gl-kpi__icon-box">
|
||||
<fa-icon icon="arrow-up"></fa-icon>
|
||||
</div>
|
||||
<div class="gl-kpi__trend gl-kpi__trend--dr" matTooltip="Sum of all debit entries in period">
|
||||
<fa-icon icon="arrow-up"></fa-icon> DR
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__body">
|
||||
<div class="gl-kpi__label">Total Debit</div>
|
||||
<div class="gl-kpi__value gl-kpi__value--dr">{{ totalDebit | formatNumber }}</div>
|
||||
<div class="gl-kpi__caption">Debit entries in period</div>
|
||||
</div>
|
||||
<div class="gl-kpi__bar gl-kpi__bar--dr"></div>
|
||||
</div>
|
||||
|
||||
<!-- Total Credit -->
|
||||
<div class="gl-kpi gl-kpi--cr">
|
||||
<div class="gl-kpi__header">
|
||||
<div class="gl-kpi__icon-box">
|
||||
<fa-icon icon="arrow-down"></fa-icon>
|
||||
</div>
|
||||
<div class="gl-kpi__trend gl-kpi__trend--cr" matTooltip="Sum of all credit entries in period">
|
||||
<fa-icon icon="arrow-down"></fa-icon> CR
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__body">
|
||||
<div class="gl-kpi__label">Total Credit</div>
|
||||
<div class="gl-kpi__value gl-kpi__value--cr">{{ totalCredit | formatNumber }}</div>
|
||||
<div class="gl-kpi__caption">Credit entries in period</div>
|
||||
</div>
|
||||
<div class="gl-kpi__bar gl-kpi__bar--cr"></div>
|
||||
</div>
|
||||
|
||||
<!-- Net Movement -->
|
||||
<div class="gl-kpi gl-kpi--net">
|
||||
<div class="gl-kpi__header">
|
||||
<div class="gl-kpi__icon-box">
|
||||
<fa-icon icon="scale-balanced"></fa-icon>
|
||||
</div>
|
||||
<div
|
||||
[class]="netMovementPositive ? 'gl-kpi__trend gl-kpi__trend--pos' : 'gl-kpi__trend gl-kpi__trend--neg'"
|
||||
[matTooltip]="isDebitNormal ? 'DR − CR (Debit-normal account)' : 'CR − DR (Credit-normal account)'"
|
||||
>
|
||||
<fa-icon [icon]="netMovementPositive ? 'arrow-up' : 'arrow-down'"></fa-icon>
|
||||
<span class="gl-kpi__norm-tag">{{ isDebitNormal ? 'DR−CR' : 'CR−DR' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__body">
|
||||
<div class="gl-kpi__label">Net Movement</div>
|
||||
<div
|
||||
class="gl-kpi__value"
|
||||
[class.gl-kpi__value--pos]="netMovementPositive"
|
||||
[class.gl-kpi__value--neg]="!netMovementPositive"
|
||||
>
|
||||
{{ netMovement | formatNumber }}
|
||||
</div>
|
||||
<div class="gl-kpi__caption">
|
||||
{{ netMovementPositive ? 'Net Positive' : 'Net Negative' }} ·
|
||||
{{ isDebitNormal ? 'Debit Normal' : 'Credit Normal' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__bar gl-kpi__bar--net"></div>
|
||||
</div>
|
||||
|
||||
<!-- Closing Balance -->
|
||||
<div class="gl-kpi gl-kpi--closing">
|
||||
<div class="gl-kpi__header">
|
||||
<div class="gl-kpi__icon-box">
|
||||
<fa-icon icon="box-open"></fa-icon>
|
||||
</div>
|
||||
<div
|
||||
[class]="closingBalance >= 0 ? 'gl-kpi__trend gl-kpi__trend--pos' : 'gl-kpi__trend gl-kpi__trend--neg'"
|
||||
matTooltip="Opening + Net Movement"
|
||||
>
|
||||
<fa-icon [icon]="closingBalance >= 0 ? 'arrow-up' : 'arrow-down'"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__body">
|
||||
<div class="gl-kpi__label">Closing Balance</div>
|
||||
<div class="gl-kpi__value" [class.gl-kpi__value--neg]="closingBalance < 0">
|
||||
{{ closingBalance | formatNumber }}
|
||||
</div>
|
||||
<div class="gl-kpi__caption">As of {{ formatLedgerDate(ledgerData?.toDate) || 'period end' }}</div>
|
||||
</div>
|
||||
<div class="gl-kpi__bar gl-kpi__bar--closing"></div>
|
||||
</div>
|
||||
|
||||
<!-- Entry Count -->
|
||||
<div class="gl-kpi gl-kpi--count">
|
||||
<div class="gl-kpi__header">
|
||||
<div class="gl-kpi__icon-box">
|
||||
<fa-icon icon="receipt"></fa-icon>
|
||||
</div>
|
||||
<div class="gl-kpi__trend gl-kpi__trend--neutral" matTooltip="Total filtered records">
|
||||
<fa-icon icon="filter"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-kpi__body">
|
||||
<div class="gl-kpi__label">Total Entries</div>
|
||||
<div class="gl-kpi__value gl-kpi__value--count">{{ totalEntries || (dataSource?.records$ | async) || 0 }}</div>
|
||||
<div class="gl-kpi__caption">Matching journal entries</div>
|
||||
</div>
|
||||
<div class="gl-kpi__bar gl-kpi__bar--count"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
QUICK DATE CHIPS
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-quick-bar">
|
||||
<div class="gl-quick-bar__label"><fa-icon icon="clock"></fa-icon> Quick Range</div>
|
||||
<div class="gl-quick-bar__chips">
|
||||
<button
|
||||
*ngFor="let q of quickFilters"
|
||||
[class.active]="activeQuickFilter === q.label"
|
||||
class="gl-qchip"
|
||||
(click)="applyQuickFilter(q)"
|
||||
>
|
||||
<fa-icon [icon]="q.icon"></fa-icon>
|
||||
{{ q.label }}
|
||||
</button>
|
||||
<button *ngIf="activeQuickFilter" class="gl-qchip gl-qchip--clear" (click)="clearQuickFilter()">
|
||||
<fa-icon icon="times"></fa-icon> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
FILTER PANEL
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-filter-panel">
|
||||
<!-- Primary filters -->
|
||||
<div class="gl-filter-panel__primary">
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="calendar-alt"></fa-icon> From Date</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<input matInput [matDatepicker]="dpFrom" [min]="minDate" [max]="maxDate" [formControl]="transactionDateFrom" />
|
||||
<mat-datepicker-toggle matSuffix [for]="dpFrom"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dpFrom></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="calendar-alt"></fa-icon> To Date</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<input matInput [matDatepicker]="dpTo" [min]="minDate" [max]="maxDate" [formControl]="transactionDateTo" />
|
||||
<mat-datepicker-toggle matSuffix [for]="dpTo"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dpTo></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="gl-fgroup gl-fgroup--wide">
|
||||
<label class="gl-flabel"><fa-icon icon="building-columns"></fa-icon> GL Account</label>
|
||||
<div class="gl-ffield gl-ffield--selector">
|
||||
<mifosx-gl-account-selector
|
||||
[inputFormControl]="glAccount"
|
||||
[glAccountList]="glAccountData"
|
||||
></mifosx-gl-account-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="toggle-on"></fa-icon> Entry Source</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<mat-select [formControl]="entryTypeFilter">
|
||||
<mat-option *ngFor="let et of entryTypeFilterData" [value]="et.value">{{ et.option }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="gl-filter-actions">
|
||||
<button
|
||||
class="gl-fab gl-fab--adv"
|
||||
(click)="showAdvancedFilters = !showAdvancedFilters"
|
||||
[class.gl-fab--adv-on]="showAdvancedFilters"
|
||||
matTooltip="More filters"
|
||||
>
|
||||
<fa-icon icon="sliders-h"></fa-icon>
|
||||
<span *ngIf="activeFiltersCount > 0" class="gl-fab__badge">{{ activeFiltersCount }}</span>
|
||||
</button>
|
||||
<button class="gl-fab gl-fab--reset" (click)="clearFilters()" matTooltip="Reset all filters">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced filters (collapsible) -->
|
||||
<div class="gl-filter-panel__advanced" [class.gl-filter-panel__advanced--open]="showAdvancedFilters">
|
||||
<div class="gl-filter-panel__adv-inner">
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="university"></fa-icon> Office</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<input matInput [formControl]="officeName" [matAutocomplete]="officeAc" placeholder="Search office…" />
|
||||
<mat-autocomplete #officeAc="matAutocomplete" [displayWith]="displayOfficeName">
|
||||
<mat-option *ngFor="let o of filteredOfficeData | async" [value]="o">{{ o.name }}</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="hashtag"></fa-icon> Transaction ID</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<input matInput [formControl]="transactionIdCtrl" placeholder="e.g. L000001" />
|
||||
<fa-icon icon="search" matSuffix class="gl-field-suffix-icon"></fa-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="calendar-check"></fa-icon> Submitted From</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="dpSubFrom"
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[formControl]="submittedOnDateFrom"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="dpSubFrom"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dpSubFrom></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="gl-fgroup">
|
||||
<label class="gl-flabel"><fa-icon icon="calendar-check"></fa-icon> Submitted To</label>
|
||||
<mat-form-field appearance="outline" class="gl-ffield">
|
||||
<input matInput [matDatepicker]="dpSubTo" [min]="minDate" [max]="maxDate" [formControl]="submittedOnDateTo" />
|
||||
<mat-datepicker-toggle matSuffix [for]="dpSubTo"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dpSubTo></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
TIP CARD — shown when no GL account selected
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-tip" *ngIf="!hasGlAccountFilter">
|
||||
<div class="gl-tip__icon"><fa-icon icon="lightbulb"></fa-icon></div>
|
||||
<div class="gl-tip__body">
|
||||
<strong>Select a GL Account</strong> to unlock full ledger details — opening balance, day-wise running balance,
|
||||
closing balance, and per-entry running totals.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
DAY-WISE BALANCE TIMELINE
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-timeline-section" *ngIf="dayWiseData.length > 0">
|
||||
<div class="gl-section-head">
|
||||
<div class="gl-section-head__left">
|
||||
<div class="gl-section-head__dot gl-section-head__dot--purple"></div>
|
||||
<span class="gl-section-head__title">Day-wise Balance Timeline</span>
|
||||
<span class="gl-section-head__badge">{{ dayWiseData.length }} days</span>
|
||||
</div>
|
||||
<div class="gl-section-head__right">
|
||||
<span class="gl-legend-pill gl-legend-pill--open">Opening</span>
|
||||
<span class="gl-legend-pill gl-legend-pill--dr">Debit</span>
|
||||
<span class="gl-legend-pill gl-legend-pill--cr">Credit</span>
|
||||
<span class="gl-legend-pill gl-legend-pill--net">Net</span>
|
||||
<span class="gl-legend-pill gl-legend-pill--close">Closing</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gl-timeline-wrap">
|
||||
<table class="gl-tl-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="gl-tl-th">Date</th>
|
||||
<th class="gl-tl-th gl-tl-th--num">Opening Bal</th>
|
||||
<th class="gl-tl-th gl-tl-th--num gl-tl-th--dr">Debit (DR)</th>
|
||||
<th class="gl-tl-th gl-tl-th--num gl-tl-th--cr">Credit (CR)</th>
|
||||
<th class="gl-tl-th gl-tl-th--bar">Movement</th>
|
||||
<th class="gl-tl-th gl-tl-th--num">Net</th>
|
||||
<th class="gl-tl-th gl-tl-th--num gl-tl-th--close">Closing Bal</th>
|
||||
<th class="gl-tl-th gl-tl-th--ctr">Entries</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="let row of dayWiseData; let i = index"
|
||||
class="gl-tl-row"
|
||||
[class.gl-tl-row--first]="i === 0"
|
||||
[class.gl-tl-row--last]="i === dayWiseData.length - 1"
|
||||
>
|
||||
<td class="gl-tl-date">
|
||||
<div class="gl-tl-date__inner">
|
||||
<div
|
||||
class="gl-tl-date__dot"
|
||||
[class.gl-tl-date__dot--up]="row.netMovement >= 0"
|
||||
[class.gl-tl-date__dot--down]="row.netMovement < 0"
|
||||
></div>
|
||||
<div class="gl-tl-date__text">{{ formatLedgerDate(row.date) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="gl-tl-num">{{ row.openingBalance | formatNumber }}</td>
|
||||
|
||||
<td class="gl-tl-num gl-tl-num--dr">
|
||||
<span *ngIf="row.totalDebit > 0">{{ row.totalDebit | formatNumber }}</span>
|
||||
<span *ngIf="row.totalDebit === 0" class="gl-tl-nil">—</span>
|
||||
</td>
|
||||
|
||||
<td class="gl-tl-num gl-tl-num--cr">
|
||||
<span *ngIf="row.totalCredit > 0">{{ row.totalCredit | formatNumber }}</span>
|
||||
<span *ngIf="row.totalCredit === 0" class="gl-tl-nil">—</span>
|
||||
</td>
|
||||
|
||||
<!-- Mini visual bar — DR vs CR side by side as % of their combined total -->
|
||||
<td class="gl-tl-bar-cell">
|
||||
<div class="gl-minibar">
|
||||
<div
|
||||
class="gl-minibar__dr"
|
||||
[style.flex]="row.totalDebit"
|
||||
matTooltip="DR: {{ row.totalDebit | formatNumber }}"
|
||||
></div>
|
||||
<div
|
||||
class="gl-minibar__cr"
|
||||
[style.flex]="row.totalCredit"
|
||||
matTooltip="CR: {{ row.totalCredit | formatNumber }}"
|
||||
></div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="gl-tl-num"
|
||||
[class.gl-tl-num--pos]="row.netMovement >= 0"
|
||||
[class.gl-tl-num--neg]="row.netMovement < 0"
|
||||
>
|
||||
<fa-icon [icon]="row.netMovement >= 0 ? 'arrow-up' : 'arrow-down'" class="gl-tl-net-icon"></fa-icon>
|
||||
{{ row.netMovement | formatNumber }}
|
||||
</td>
|
||||
|
||||
<td class="gl-tl-num gl-tl-num--close" [class.gl-tl-num--neg]="row.closingBalance < 0">
|
||||
<strong>{{ row.closingBalance | formatNumber }}</strong>
|
||||
</td>
|
||||
|
||||
<td class="gl-tl-ctr">
|
||||
<span class="gl-tl-badge">{{ row.entryCount }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="gl-tl-footer">
|
||||
<td class="gl-tl-footer__label">Period Total</td>
|
||||
<td class="gl-tl-num">{{ openingBalance | formatNumber }}</td>
|
||||
<td class="gl-tl-num gl-tl-num--dr">
|
||||
<strong>{{ totalDebit | formatNumber }}</strong>
|
||||
</td>
|
||||
<td class="gl-tl-num gl-tl-num--cr">
|
||||
<strong>{{ totalCredit | formatNumber }}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
<td
|
||||
class="gl-tl-num"
|
||||
[class.gl-tl-num--pos]="netMovementPositive"
|
||||
[class.gl-tl-num--neg]="!netMovementPositive"
|
||||
>
|
||||
<strong>{{ netMovement | formatNumber }}</strong>
|
||||
</td>
|
||||
<td class="gl-tl-num gl-tl-num--close" [class.gl-tl-num--neg]="closingBalance < 0">
|
||||
<strong>{{ closingBalance | formatNumber }}</strong>
|
||||
</td>
|
||||
<td class="gl-tl-ctr">
|
||||
<span class="gl-tl-badge gl-tl-badge--total">{{ (dataSource?.records$ | async) || 0 }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
JOURNAL ENTRIES TABLE
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="gl-entries-section">
|
||||
<div class="gl-section-head">
|
||||
<div class="gl-section-head__left">
|
||||
<div class="gl-section-head__dot gl-section-head__dot--blue"></div>
|
||||
<span class="gl-section-head__title">Journal Entries</span>
|
||||
<span class="gl-section-head__badge">{{ (dataSource?.records$ | async) || 0 }}</span>
|
||||
</div>
|
||||
<div class="gl-section-head__right">
|
||||
<div class="gl-entries-totals">
|
||||
<span class="gl-entries-totals__dr">
|
||||
<fa-icon icon="arrow-up"></fa-icon> DR <strong>{{ totalDebit | formatNumber }}</strong>
|
||||
</span>
|
||||
<div class="gl-entries-totals__sep"></div>
|
||||
<span class="gl-entries-totals__cr">
|
||||
<fa-icon icon="arrow-down"></fa-icon> CR <strong>{{ totalCredit | formatNumber }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gl-table-shell">
|
||||
<mat-table [dataSource]="dataSource" matSort class="gl-mat-table">
|
||||
<!-- #ID -->
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">#</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<a [routerLink]="['/accounting/journal-entries/transactions/view', e.transactionId]" class="gl-id-link"
|
||||
>#{{ e.id }}</a
|
||||
>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Transaction Date -->
|
||||
<ng-container matColumnDef="transactionDate">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">Date</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<div class="gl-date-cell">
|
||||
<div class="gl-date-cell__main">{{ formatLedgerDate(e.transactionDate) }}</div>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Transaction ID -->
|
||||
<ng-container matColumnDef="transactionId">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">Txn ID</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<a
|
||||
[routerLink]="['/accounting/journal-entries/transactions/view', e.transactionId]"
|
||||
class="gl-txn-link gl-mono"
|
||||
matTooltip="{{ e.transactionId }}"
|
||||
>
|
||||
{{ e.transactionId }}
|
||||
</a>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Office -->
|
||||
<ng-container matColumnDef="officeName">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">Office</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<div class="gl-office-cell">
|
||||
<fa-icon icon="university" class="gl-office-icon"></fa-icon>
|
||||
{{ e.officeName }}
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- GL Code -->
|
||||
<ng-container matColumnDef="glAccountCode">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">Code</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<span class="gl-code-chip">{{ e.glAccountCode }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- GL Account Name -->
|
||||
<ng-container matColumnDef="glAccountName">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">Account</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<span class="gl-acct-cell" matTooltip="{{ e.glAccountName }}">{{ e.glAccountName }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Account Type -->
|
||||
<ng-container matColumnDef="glAccountType">
|
||||
<mat-header-cell *matHeaderCellDef class="gl-th">Type</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<span [class]="'gl-pill gl-pill--sm gl-pill--' + (e.glAccountType?.value | lowercase)">
|
||||
<fa-icon [icon]="getAccountTypeIcon(e.glAccountType?.value)" class="gl-pill__icon"></fa-icon>
|
||||
{{ e.glAccountType?.value }}
|
||||
</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Entry Type DR/CR -->
|
||||
<ng-container matColumnDef="entryType">
|
||||
<mat-header-cell *matHeaderCellDef class="gl-th">Type</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<span [class]="e.entryType?.value === 'DEBIT' ? 'gl-drcr gl-drcr--dr' : 'gl-drcr gl-drcr--cr'">
|
||||
{{ e.entryType?.value === 'DEBIT' ? 'DR' : 'CR' }}
|
||||
</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Debit -->
|
||||
<ng-container matColumnDef="debit">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th gl-th--num">Debit</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td gl-td--num">
|
||||
<span *ngIf="e.entryType?.value === 'DEBIT'" class="gl-amt gl-amt--dr">{{ e.amount | formatNumber }}</span>
|
||||
<span *ngIf="e.entryType?.value !== 'DEBIT'" class="gl-amt-nil">—</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Credit -->
|
||||
<ng-container matColumnDef="credit">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th gl-th--num">Credit</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td gl-td--num">
|
||||
<span *ngIf="e.entryType?.value === 'CREDIT'" class="gl-amt gl-amt--cr">{{ e.amount | formatNumber }}</span>
|
||||
<span *ngIf="e.entryType?.value !== 'CREDIT'" class="gl-amt-nil">—</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Running Balance -->
|
||||
<ng-container matColumnDef="runningBalance">
|
||||
<mat-header-cell *matHeaderCellDef class="gl-th gl-th--num">Running Bal</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td gl-td--num">
|
||||
<span
|
||||
class="gl-running"
|
||||
[class.gl-running--neg]="getRunningBalance(e) < 0"
|
||||
matTooltip="Office running balance"
|
||||
>
|
||||
{{ getRunningBalance(e) | formatNumber }}
|
||||
</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Source (Manual/System) -->
|
||||
<ng-container matColumnDef="manualEntry">
|
||||
<mat-header-cell *matHeaderCellDef class="gl-th">Source</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<span [class]="e.manualEntry ? 'gl-src gl-src--manual' : 'gl-src gl-src--system'">
|
||||
<fa-icon [icon]="e.manualEntry ? 'pencil-alt' : 'cog'"></fa-icon>
|
||||
{{ e.manualEntry ? 'Manual' : 'System' }}
|
||||
</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Created By -->
|
||||
<ng-container matColumnDef="createdByUserName">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">By</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<div class="gl-user-cell">
|
||||
<div class="gl-user-avatar">{{ (e.createdByUserName || 'U').charAt(0).toUpperCase() }}</div>
|
||||
<span>{{ e.createdByUserName }}</span>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Submitted On -->
|
||||
<ng-container matColumnDef="submittedOnDate">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="gl-th">Submitted</mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<span class="gl-ts">{{ e.submittedOnDate | datetimeFormat }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef class="gl-th"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let e" class="gl-td">
|
||||
<a
|
||||
[routerLink]="['/accounting/journal-entries/transactions/view', e.transactionId]"
|
||||
class="gl-view-btn"
|
||||
matTooltip="View transaction"
|
||||
>
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</a>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="gl-header-row"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[class.gl-data-row--dr]="row.entryType?.value === 'DEBIT'"
|
||||
[class.gl-data-row--cr]="row.entryType?.value === 'CREDIT'"
|
||||
class="gl-data-row"
|
||||
></mat-row>
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell" [attr.colspan]="displayedColumns.length">
|
||||
<div class="gl-empty">
|
||||
<div class="gl-empty__icon"><fa-icon icon="search"></fa-icon></div>
|
||||
<div class="gl-empty__title">No entries found</div>
|
||||
<div class="gl-empty__sub">Try adjusting your filters or date range</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</mat-table>
|
||||
|
||||
<div class="gl-pager-wrap">
|
||||
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,26 +6,20 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, ViewChild, AfterViewInit, inject } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit, inject } from '@angular/core';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortHeader } from '@angular/material/sort';
|
||||
import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
/** rxjs Imports */
|
||||
import { merge } from 'rxjs';
|
||||
import { tap, startWith, map, distinctUntilChanged, debounceTime } from 'rxjs/operators';
|
||||
|
||||
/** Custom Services */
|
||||
import { AccountingService } from '../accounting.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
/** Custom Data Source */
|
||||
import { JournalEntriesDataSource } from './journal-entry.datasource';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { MatAutocompleteTrigger, MatOption, MatAutocomplete } from '@angular/material/autocomplete';
|
||||
import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
|
||||
import { GlAccountSelectorComponent } from '../../shared/accounting/gl-account-selector/gl-account-selector.component';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { AsyncPipe, NgClass, NgIf, NgFor, DecimalPipe, CurrencyPipe } from '@angular/common';
|
||||
import {
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
@@ -36,18 +30,23 @@ import {
|
||||
MatHeaderRowDef,
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow
|
||||
MatRow,
|
||||
MatNoDataRow
|
||||
} from '@angular/material/table';
|
||||
import { DateFormatPipe } from '../../pipes/date-format.pipe';
|
||||
import { DatetimeFormatPipe } from '../../pipes/datetime-format.pipe';
|
||||
import { FormatNumberPipe } from '../../pipes/format-number.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
const STATE_KEY = 'gl_ledger_filter_state';
|
||||
|
||||
/**
|
||||
* Search journal entry component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-search-journal-entry',
|
||||
standalone: true,
|
||||
templateUrl: './search-journal-entry.component.html',
|
||||
styleUrls: ['./search-journal-entry.component.scss'],
|
||||
imports: [
|
||||
@@ -67,97 +66,120 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatNoDataRow,
|
||||
MatPaginator,
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgIf,
|
||||
NgFor,
|
||||
DateFormatPipe,
|
||||
DatetimeFormatPipe,
|
||||
FormatNumberPipe
|
||||
FormatNumberPipe,
|
||||
FaIconComponent,
|
||||
MatCardModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
RouterLink
|
||||
]
|
||||
})
|
||||
export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
export class SearchJournalEntryComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private accountingService = inject(AccountingService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
/** Minimum transaction date allowed. */
|
||||
// ── Misc ──────────────────────────────────────────────────────────────────
|
||||
minDate = new Date(2000, 0, 1);
|
||||
/** Maximum transaction date allowed. */
|
||||
maxDate = new Date();
|
||||
/** Office name filter form control. */
|
||||
|
||||
// ── Filter controls ───────────────────────────────────────────────────────
|
||||
officeName = new UntypedFormControl();
|
||||
/** Office data. */
|
||||
officeData: any;
|
||||
/** Filtered office data for autocomplete. */
|
||||
filteredOfficeData: any;
|
||||
/** Gl Account filter form control. */
|
||||
glAccount = new UntypedFormControl();
|
||||
/** Gl Account data. */
|
||||
glAccountData: any;
|
||||
/** Filtered gl account data. */
|
||||
filteredGLAccountData: any;
|
||||
/** Entry type filter form control. */
|
||||
entryTypeFilter = new UntypedFormControl('');
|
||||
/** Entry type filter data. */
|
||||
entryTypeFilterData = [
|
||||
{
|
||||
option: 'labels.inputs.All',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
option: 'labels.inputs.Manual Entries',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
option: 'labels.inputs.System Entries',
|
||||
value: false // Bug: unable to implement from server side
|
||||
}
|
||||
];
|
||||
/** Transaction date from form control. */
|
||||
transactionDateFrom = new UntypedFormControl(new Date(new Date().setMonth(new Date().getMonth() - 1)));
|
||||
/** Transaction date to form control. */
|
||||
transactionDateTo = new UntypedFormControl(new Date());
|
||||
/** Transaction ID form control. */
|
||||
transactionId = new UntypedFormControl();
|
||||
/** Submitted on date from form control. */
|
||||
transactionIdCtrl = new UntypedFormControl();
|
||||
submittedOnDateFrom = new UntypedFormControl();
|
||||
/** Submitted on date to form control. */
|
||||
submittedOnDateTo = new UntypedFormControl();
|
||||
/** Columns to be displayed in journal entries table. */
|
||||
referenceNumber = new UntypedFormControl();
|
||||
|
||||
entryTypeFilterData = [
|
||||
{ option: 'All Entries', value: '' },
|
||||
{ option: 'Manual Entries', value: 'true' },
|
||||
{ option: 'System Entries', value: 'false' }
|
||||
];
|
||||
|
||||
// ── Table columns ─────────────────────────────────────────────────────────
|
||||
displayedColumns: string[] = [
|
||||
'id',
|
||||
'officeName',
|
||||
'transactionId',
|
||||
'transactionDate',
|
||||
'glAccountType',
|
||||
'createdByUserName',
|
||||
'submittedOnDate',
|
||||
'transactionId',
|
||||
'officeName',
|
||||
'glAccountCode',
|
||||
'glAccountName',
|
||||
'currency',
|
||||
'glAccountType',
|
||||
'entryType',
|
||||
'debit',
|
||||
'credit'
|
||||
'credit',
|
||||
'runningBalance',
|
||||
'manualEntry',
|
||||
'createdByUserName',
|
||||
'submittedOnDate',
|
||||
'actions'
|
||||
];
|
||||
/** Data source for journal entries table. */
|
||||
dayWiseColumns = [
|
||||
'date',
|
||||
'openingBalance',
|
||||
'totalDebit',
|
||||
'totalCredit',
|
||||
'netMovement',
|
||||
'closingBalance',
|
||||
'entryCount'
|
||||
];
|
||||
|
||||
dataSource: JournalEntriesDataSource;
|
||||
/** Journal entries filter. */
|
||||
|
||||
// ── Ledger summary (from backend /ledger endpoint) ────────────────────────
|
||||
ledgerData: any = null;
|
||||
ledgerLoading = false;
|
||||
|
||||
openingBalance = 0;
|
||||
closingBalance = 0;
|
||||
totalDebit = 0;
|
||||
totalCredit = 0;
|
||||
netMovement = 0;
|
||||
totalEntries = 0;
|
||||
isDebitNormal = true;
|
||||
dayWiseData: any[] = [];
|
||||
glAccountInfo: any = null;
|
||||
|
||||
// ── UI state ──────────────────────────────────────────────────────────────
|
||||
activeQuickFilter = '';
|
||||
activeFiltersCount = 0;
|
||||
showAdvancedFilters = false;
|
||||
selectedGlAccountId: number | null = null;
|
||||
|
||||
// ── Quick date filter presets ─────────────────────────────────────────────
|
||||
quickFilters = [
|
||||
{ label: 'Today', icon: 'calendar-day', type: 'day' },
|
||||
{ label: 'Yesterday', icon: 'calendar-alt', type: 'prev' },
|
||||
{ label: 'This Week', icon: 'calendar-week', type: 'week' },
|
||||
{ label: 'Last Week', icon: 'calendar-week', type: 'lastweek' },
|
||||
{ label: 'This Month', icon: 'calendar-alt', type: 'month' },
|
||||
{ label: 'Last Month', icon: 'calendar-alt', type: 'lastmonth' },
|
||||
{ label: 'Last 3 Mo', icon: 'chart-area', type: 'range', days: 90 },
|
||||
{ label: 'Last 6 Mo', icon: 'chart-area', type: 'range', days: 180 },
|
||||
{ label: 'This Year', icon: 'calendar-check', type: 'year' }
|
||||
];
|
||||
|
||||
filterJournalEntriesBy = [
|
||||
{
|
||||
type: 'officeId',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
type: 'glAccountId',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
type: 'manualEntriesOnly',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
type: 'transactionId',
|
||||
value: ''
|
||||
},
|
||||
{ type: 'officeId', value: '' },
|
||||
{ type: 'glAccountId', value: '' },
|
||||
{ type: 'manualEntriesOnly', value: '' },
|
||||
{ type: 'transactionId', value: '' },
|
||||
{
|
||||
type: 'fromDate',
|
||||
value: this.dateUtils.formatDate(
|
||||
@@ -165,39 +187,16 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
this.settingsService.dateFormat
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'toDate',
|
||||
value: this.dateUtils.formatDate(new Date(), this.settingsService.dateFormat)
|
||||
},
|
||||
{
|
||||
type: 'submittedOnDateFrom',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
type: 'submittedOnDateTo',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
type: 'dateFormat',
|
||||
value: this.settingsService.dateFormat
|
||||
},
|
||||
{
|
||||
type: 'locale',
|
||||
value: this.settingsService.language.code
|
||||
}
|
||||
{ type: 'toDate', value: this.dateUtils.formatDate(new Date(), this.settingsService.dateFormat) },
|
||||
{ type: 'submittedOnDateFrom', value: '' },
|
||||
{ type: 'submittedOnDateTo', value: '' },
|
||||
{ type: 'dateFormat', value: this.settingsService.dateFormat },
|
||||
{ type: 'locale', value: this.settingsService.language.code }
|
||||
];
|
||||
|
||||
/** Paginator for journal entries table. */
|
||||
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||
/** Sorter for journal entries table. */
|
||||
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||
|
||||
/**
|
||||
* Retrieves the offices and gl accounts data from `resolve`.
|
||||
* @param {AccountingService} accountingService Accounting Service.
|
||||
* @param {ActivatedRoute} route Activated Route.
|
||||
* @param {SettingsService} settingsService Settings Service.
|
||||
*/
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { offices: any; glAccounts: any }) => {
|
||||
this.officeData = data.offices;
|
||||
@@ -205,51 +204,130 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets filtered offices and gl accounts for autocomplete and journal entries table.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
this.paginator.pageSize = 10;
|
||||
this.restoreState();
|
||||
this.setFilteredOffices();
|
||||
this.setFilteredGlAccounts();
|
||||
this.getJournalEntries();
|
||||
this.loadLedgerSummary();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
private saveState() {
|
||||
const fromFilter = this.filterJournalEntriesBy.find((f) => f.type === 'fromDate');
|
||||
const toFilter = this.filterJournalEntriesBy.find((f) => f.type === 'toDate');
|
||||
const state = {
|
||||
fromDateFilter: fromFilter?.value || '',
|
||||
toDateFilter: toFilter?.value || '',
|
||||
fromDateIso: this.transactionDateFrom.value ? new Date(this.transactionDateFrom.value).toISOString() : '',
|
||||
toDateIso: this.transactionDateTo.value ? new Date(this.transactionDateTo.value).toISOString() : '',
|
||||
glAccountId: this.selectedGlAccountId,
|
||||
glAccountValue: this.glAccount.value,
|
||||
entryType: this.entryTypeFilter.value,
|
||||
activeQuickFilter: this.activeQuickFilter,
|
||||
pageIndex: this.paginator.pageIndex,
|
||||
pageSize: this.paginator.pageSize
|
||||
};
|
||||
sessionStorage.setItem(STATE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
private restoreState() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STATE_KEY);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const state = JSON.parse(raw);
|
||||
|
||||
// Restore date pickers and filter strings
|
||||
if (state.fromDateIso) {
|
||||
this.transactionDateFrom.setValue(new Date(state.fromDateIso), { emitEvent: false });
|
||||
const idx = this.filterJournalEntriesBy.findIndex((f) => f.type === 'fromDate');
|
||||
if (idx >= 0) {
|
||||
this.filterJournalEntriesBy[idx].value = state.fromDateFilter;
|
||||
}
|
||||
}
|
||||
if (state.toDateIso) {
|
||||
this.transactionDateTo.setValue(new Date(state.toDateIso), { emitEvent: false });
|
||||
const idx = this.filterJournalEntriesBy.findIndex((f) => f.type === 'toDate');
|
||||
if (idx >= 0) {
|
||||
this.filterJournalEntriesBy[idx].value = state.toDateFilter;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore GL account — set the form value so the selector shows it
|
||||
if (state.glAccountId) {
|
||||
this.selectedGlAccountId = state.glAccountId;
|
||||
this.glAccount.setValue(state.glAccountValue, { emitEvent: false });
|
||||
const idx = this.filterJournalEntriesBy.findIndex((f) => f.type === 'glAccountId');
|
||||
if (idx >= 0) {
|
||||
this.filterJournalEntriesBy[idx].value = String(state.glAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore entry type filter
|
||||
if (state.entryType !== undefined && state.entryType !== null) {
|
||||
this.entryTypeFilter.setValue(state.entryType, { emitEvent: false });
|
||||
const idx = this.filterJournalEntriesBy.findIndex((f) => f.type === 'manualEntriesOnly');
|
||||
if (idx >= 0) {
|
||||
this.filterJournalEntriesBy[idx].value = state.entryType;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.activeQuickFilter) {
|
||||
this.activeQuickFilter = state.activeQuickFilter;
|
||||
}
|
||||
if (state.pageIndex != null) {
|
||||
this.paginator.pageIndex = state.pageIndex;
|
||||
}
|
||||
if (state.pageSize != null) {
|
||||
this.paginator.pageSize = state.pageSize;
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore corrupt state */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to all search filters:
|
||||
* Office Name, GL Account, Transaction ID, Transaction Date From, Transaction Date To,
|
||||
* sort change and page change.
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this.officeName.valueChanges
|
||||
.pipe(
|
||||
map((value) => (value.id ? value.id : '')),
|
||||
map((v) => (v?.id ? String(v.id) : '')),
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(filterValue, 'officeId');
|
||||
})
|
||||
tap((val) => this.applyFilter(val, 'officeId'))
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.glAccount.valueChanges
|
||||
.pipe(
|
||||
map((value) => (value ? value : '')),
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(filterValue, 'glAccountId');
|
||||
tap((val) => {
|
||||
// GlAccountSelectorComponent emits the account id (number) directly
|
||||
const id = val != null && val !== '' ? String(val) : '';
|
||||
this.selectedGlAccountId = id ? Number(id) : null;
|
||||
this.applyFilter(id, 'glAccountId');
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.transactionId.valueChanges
|
||||
this.entryTypeFilter.valueChanges
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(filterValue, 'transactionId');
|
||||
})
|
||||
tap((val) => this.applyFilter(val ?? '', 'manualEntriesOnly'))
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.transactionIdCtrl.valueChanges
|
||||
.pipe(
|
||||
debounceTime(600),
|
||||
distinctUntilChanged(),
|
||||
tap((val) => this.applyFilter(val ?? '', 'transactionId'))
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -257,9 +335,7 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(this.dateUtils.formatDate(filterValue, this.settingsService.dateFormat), 'fromDate');
|
||||
})
|
||||
tap((val) => this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'fromDate'))
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -267,9 +343,7 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(this.dateUtils.formatDate(filterValue, this.settingsService.dateFormat), 'toDate');
|
||||
})
|
||||
tap((val) => this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'toDate'))
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -277,12 +351,9 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(
|
||||
this.dateUtils.formatDate(filterValue, this.settingsService.dateFormat),
|
||||
'submittedOnDateFrom'
|
||||
);
|
||||
})
|
||||
tap((val) =>
|
||||
this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'submittedOnDateFrom')
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -290,106 +361,181 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap((filterValue) => {
|
||||
this.applyFilter(
|
||||
this.dateUtils.formatDate(filterValue, this.settingsService.dateFormat),
|
||||
'submittedOnDateTo'
|
||||
);
|
||||
})
|
||||
tap((val) =>
|
||||
this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'submittedOnDateTo')
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
|
||||
|
||||
merge(this.sort.sortChange, this.paginator.page)
|
||||
.pipe(tap(() => this.loadJournalEntriesPage()))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a page of journal entries.
|
||||
*/
|
||||
// ── Ledger Summary (backend-computed) ─────────────────────────────────────
|
||||
loadLedgerSummary() {
|
||||
this.ledgerLoading = true;
|
||||
const fromFilter = this.filterJournalEntriesBy.find((f) => f.type === 'fromDate');
|
||||
const toFilter = this.filterJournalEntriesBy.find((f) => f.type === 'toDate');
|
||||
const offFilter = this.filterJournalEntriesBy.find((f) => f.type === 'officeId');
|
||||
const manFilter = this.filterJournalEntriesBy.find((f) => f.type === 'manualEntriesOnly');
|
||||
const txnFilter = this.filterJournalEntriesBy.find((f) => f.type === 'transactionId');
|
||||
|
||||
this.accountingService
|
||||
.getGLAccountLedger({
|
||||
glAccountId: this.selectedGlAccountId ?? undefined,
|
||||
officeId: offFilter?.value ? Number(offFilter.value) : undefined,
|
||||
fromDate: fromFilter?.value || undefined,
|
||||
toDate: toFilter?.value || undefined,
|
||||
dateFormat: this.settingsService.dateFormat,
|
||||
locale: this.settingsService.language.code,
|
||||
manualEntriesOnly: manFilter?.value === 'true' ? true : manFilter?.value === 'false' ? false : undefined,
|
||||
transactionId: txnFilter?.value || undefined,
|
||||
limit: 0, // summary only — no entries page needed from this call
|
||||
offset: 0
|
||||
})
|
||||
.subscribe({
|
||||
next: (data: any) => {
|
||||
this.ledgerData = data;
|
||||
this.glAccountInfo = data.glAccount;
|
||||
this.openingBalance = Number(data.openingBalance ?? 0);
|
||||
this.closingBalance = Number(data.closingBalance ?? 0);
|
||||
this.totalDebit = Number(data.totalDebit ?? 0);
|
||||
this.totalCredit = Number(data.totalCredit ?? 0);
|
||||
this.netMovement = Number(data.netMovement ?? 0);
|
||||
this.totalEntries = Number(data.totalEntries ?? 0);
|
||||
this.isDebitNormal = data.debitNormal !== false;
|
||||
this.dayWiseData = (data.dayWiseSummary || []).map((d: any) => ({
|
||||
date: d.date,
|
||||
openingBalance: Number(d.openingBalance ?? 0),
|
||||
totalDebit: Number(d.totalDebit ?? 0),
|
||||
totalCredit: Number(d.totalCredit ?? 0),
|
||||
netMovement: Number(d.netMovement ?? 0),
|
||||
closingBalance: Number(d.closingBalance ?? 0),
|
||||
entryCount: d.entryCount || 0
|
||||
}));
|
||||
this.updateActiveFiltersCount();
|
||||
this.ledgerLoading = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('[LedgerSummary] API error:', err);
|
||||
this.ledgerLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Quick filter ──────────────────────────────────────────────────────────
|
||||
applyQuickFilter(q: any) {
|
||||
this.activeQuickFilter = q.label;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
let from: Date, to: Date;
|
||||
|
||||
switch (q.type) {
|
||||
case 'day':
|
||||
from = new Date(today);
|
||||
to = new Date(today);
|
||||
break;
|
||||
case 'prev':
|
||||
from = new Date(today);
|
||||
from.setDate(from.getDate() - 1);
|
||||
to = new Date(from);
|
||||
break;
|
||||
case 'week': {
|
||||
const dow = today.getDay();
|
||||
from = new Date(today);
|
||||
from.setDate(today.getDate() - dow);
|
||||
to = new Date(today);
|
||||
break;
|
||||
}
|
||||
case 'lastweek': {
|
||||
const dow = today.getDay();
|
||||
to = new Date(today);
|
||||
to.setDate(today.getDate() - dow - 1);
|
||||
from = new Date(to);
|
||||
from.setDate(to.getDate() - 6);
|
||||
break;
|
||||
}
|
||||
case 'month':
|
||||
from = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
to = new Date(today);
|
||||
break;
|
||||
case 'lastmonth':
|
||||
from = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||
to = new Date(today.getFullYear(), today.getMonth(), 0);
|
||||
break;
|
||||
case 'year':
|
||||
from = new Date(today.getFullYear(), 0, 1);
|
||||
to = new Date(today);
|
||||
break;
|
||||
default:
|
||||
from = new Date(today);
|
||||
from.setDate(today.getDate() - (q.days || 90));
|
||||
to = new Date(today);
|
||||
}
|
||||
|
||||
this.transactionDateFrom.setValue(from, { emitEvent: false });
|
||||
this.transactionDateTo.setValue(to, { emitEvent: false });
|
||||
this.applyFilter(this.dateUtils.formatDate(from, this.settingsService.dateFormat), 'fromDate');
|
||||
this.applyFilter(this.dateUtils.formatDate(to, this.settingsService.dateFormat), 'toDate');
|
||||
}
|
||||
|
||||
clearQuickFilter() {
|
||||
this.activeQuickFilter = '';
|
||||
const dfrom = new Date(new Date().setMonth(new Date().getMonth() - 1));
|
||||
const dto = new Date();
|
||||
this.transactionDateFrom.setValue(dfrom, { emitEvent: false });
|
||||
this.transactionDateTo.setValue(dto, { emitEvent: false });
|
||||
this.applyFilter(this.dateUtils.formatDate(dfrom, this.settingsService.dateFormat), 'fromDate');
|
||||
this.applyFilter(this.dateUtils.formatDate(dto, this.settingsService.dateFormat), 'toDate');
|
||||
}
|
||||
|
||||
clearFilters() {
|
||||
sessionStorage.removeItem(STATE_KEY);
|
||||
this.officeName.setValue('', { emitEvent: false });
|
||||
this.glAccount.setValue('', { emitEvent: false });
|
||||
this.entryTypeFilter.setValue('', { emitEvent: false });
|
||||
this.transactionIdCtrl.setValue('', { emitEvent: false });
|
||||
this.submittedOnDateFrom.setValue('', { emitEvent: false });
|
||||
this.submittedOnDateTo.setValue('', { emitEvent: false });
|
||||
this.referenceNumber.setValue('', { emitEvent: false });
|
||||
this.selectedGlAccountId = null;
|
||||
this.filterJournalEntriesBy.forEach((f) => {
|
||||
if (![
|
||||
'fromDate',
|
||||
'toDate',
|
||||
'dateFormat',
|
||||
'locale'
|
||||
].includes(f.type)) f.value = '';
|
||||
});
|
||||
this.clearQuickFilter();
|
||||
this.loadLedgerSummary();
|
||||
this.loadJournalEntriesPage();
|
||||
}
|
||||
|
||||
// ── Table & pagination ────────────────────────────────────────────────────
|
||||
loadJournalEntriesPage() {
|
||||
if (!this.sort.direction) {
|
||||
delete this.sort.active;
|
||||
delete (this.sort as any).active;
|
||||
}
|
||||
this.dataSource.getJournalEntries(
|
||||
this.filterJournalEntriesBy,
|
||||
this.sort.active,
|
||||
this.sort.direction,
|
||||
this.paginator.pageIndex,
|
||||
this.paginator.pageSize
|
||||
this.paginator.pageSize || 10
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters data in journal entries table based on passed value and poperty.
|
||||
* @param {string} filterValue Value to filter data.
|
||||
* @param {string} property Property to filter data by.
|
||||
*/
|
||||
applyFilter(filterValue: string, property: string) {
|
||||
this.paginator.pageIndex = 0;
|
||||
const findIndex = this.filterJournalEntriesBy.findIndex((filter) => filter.type === property);
|
||||
this.filterJournalEntriesBy[findIndex].value = filterValue;
|
||||
const idx = this.filterJournalEntriesBy.findIndex((f) => f.type === property);
|
||||
if (idx >= 0) this.filterJournalEntriesBy[idx].value = filterValue;
|
||||
this.loadJournalEntriesPage();
|
||||
this.loadLedgerSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays office name in form control input.
|
||||
* @param {any} office Office data.
|
||||
* @returns {string} Office name if valid otherwise undefined.
|
||||
*/
|
||||
displayOfficeName(office?: any): string | undefined {
|
||||
return office ? office.name : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets filtered offices for autocomplete.
|
||||
*/
|
||||
setFilteredOffices() {
|
||||
this.filteredOfficeData = this.officeName.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((office: any) => (typeof office === 'string' ? office : office.name)),
|
||||
map((officeName: string) => (officeName ? this.filterOfficeAutocompleteData(officeName) : this.officeData))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets filtered gl accounts for autocomplete.
|
||||
*/
|
||||
setFilteredGlAccounts() {
|
||||
this.filteredGLAccountData = this.glAccount.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((glAccount: any) =>
|
||||
typeof glAccount === 'string' ? glAccount : glAccount.name + ' (' + glAccount.glCode + ')'
|
||||
),
|
||||
map((glAccount: string) => (glAccount ? this.filterGLAccountAutocompleteData(glAccount) : this.glAccountData))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters offices.
|
||||
* @param {string} officeName Office name to filter office by.
|
||||
* @returns {any} Filtered offices.
|
||||
*/
|
||||
private filterOfficeAutocompleteData(officeName: string): any {
|
||||
return this.officeData.filter((office: any) => office.name.toLowerCase().includes(officeName.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters gl accounts.
|
||||
* @param {string} glAccount Gl Account name to filter gl account by.
|
||||
* @returns {any} Filtered gl accounts.
|
||||
*/
|
||||
private filterGLAccountAutocompleteData(glAccount: string): any {
|
||||
return this.glAccountData.filter((option: any) =>
|
||||
(option.name + ' (' + option.glCode + ')').toLowerCase().includes(glAccount.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the data source for journal entries table and loads the first page.
|
||||
*/
|
||||
getJournalEntries() {
|
||||
this.dataSource = new JournalEntriesDataSource(this.accountingService);
|
||||
this.dataSource.getJournalEntries(
|
||||
@@ -397,7 +543,83 @@ export class SearchJournalEntryComponent implements OnInit, AfterViewInit {
|
||||
this.sort.active,
|
||||
this.sort.direction,
|
||||
this.paginator.pageIndex,
|
||||
this.paginator.pageSize
|
||||
this.paginator.pageSize || 10
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helper getters ────────────────────────────────────────────────────────
|
||||
get hasGlAccountFilter(): boolean {
|
||||
return !!this.selectedGlAccountId;
|
||||
}
|
||||
|
||||
get netMovementPositive(): boolean {
|
||||
return this.netMovement >= 0;
|
||||
}
|
||||
|
||||
getRunningBalance(entry: any): number {
|
||||
return Number(entry.officeRunningBalance ?? entry.organizationRunningBalance ?? 0);
|
||||
}
|
||||
|
||||
getAccountTypeBadgeClass(type: string): string {
|
||||
const m: Record<string, string> = {
|
||||
ASSET: 'gl-badge--asset',
|
||||
LIABILITY: 'gl-badge--liability',
|
||||
INCOME: 'gl-badge--income',
|
||||
EXPENSE: 'gl-badge--expense',
|
||||
EQUITY: 'gl-badge--equity'
|
||||
};
|
||||
return m[type] || 'gl-badge--default';
|
||||
}
|
||||
|
||||
getAccountTypeIcon(type: string): string {
|
||||
const m: Record<string, string> = {
|
||||
ASSET: 'building-columns',
|
||||
LIABILITY: 'file-lines',
|
||||
INCOME: 'arrow-down',
|
||||
EXPENSE: 'arrow-up',
|
||||
EQUITY: 'scale-balanced'
|
||||
};
|
||||
return m[type] || 'tag';
|
||||
}
|
||||
|
||||
getEntryTypeClass(entry: any): string {
|
||||
return entry.entryType?.value === 'DEBIT' ? 'gl-row--dr' : 'gl-row--cr';
|
||||
}
|
||||
|
||||
getDayNetClass(net: number): string {
|
||||
return net >= 0 ? 'gl-positive' : 'gl-negative';
|
||||
}
|
||||
|
||||
private updateActiveFiltersCount() {
|
||||
this.activeFiltersCount = this.filterJournalEntriesBy.filter(
|
||||
(f) => ![
|
||||
'dateFormat',
|
||||
'locale',
|
||||
'fromDate',
|
||||
'toDate'
|
||||
].includes(f.type) && f.value !== ''
|
||||
).length;
|
||||
}
|
||||
|
||||
displayOfficeName(office?: any): string | undefined {
|
||||
return office ? office.name : undefined;
|
||||
}
|
||||
|
||||
setFilteredOffices() {
|
||||
this.filteredOfficeData = this.officeName.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map((o: any) => (typeof o === 'string' ? o : o?.name || '')),
|
||||
map((n: string) =>
|
||||
n ? this.officeData?.filter((o: any) => o.name.toLowerCase().includes(n.toLowerCase())) : this.officeData
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
formatLedgerDate(dateArr: any): string {
|
||||
if (!dateArr) return '—';
|
||||
if (Array.isArray(dateArr)) {
|
||||
return `${dateArr[0]}-${String(dateArr[1]).padStart(2, '0')}-${String(dateArr[2]).padStart(2, '0')}`;
|
||||
}
|
||||
return String(dateArr).substring(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
+237
-143
@@ -7,159 +7,253 @@
|
||||
-->
|
||||
|
||||
@if (isJournalEntryLoaded) {
|
||||
<div>
|
||||
@if (dataSource?.data[0].manualEntry) {
|
||||
<div class="layout-row layout-align-end container m-b-20">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="revertTransaction()"
|
||||
*mifosxHasPermission="'REVERSE_JOURNALENTRY'"
|
||||
[disabled]="dataSource?.data[0].reversed"
|
||||
>
|
||||
@if (!dataSource.data[0].reversed) {
|
||||
<span>
|
||||
<fa-icon icon="undo" class="m-r-10"></fa-icon>{{ 'labels.buttons.Revert Transaction' | translate }}
|
||||
</span>
|
||||
}
|
||||
@if (dataSource.data[0].reversed) {
|
||||
<span>{{ 'labels.buttons.Transaction Reverted' | translate }}</span>
|
||||
}
|
||||
<div class="txn-detail-page">
|
||||
<!-- Header Bar -->
|
||||
<div class="txn-header-bar">
|
||||
<div class="txn-header-bar__left">
|
||||
<button mat-icon-button (click)="goBack()" class="txn-back-btn" matTooltip="Go Back">
|
||||
<fa-icon icon="arrow-left"></fa-icon>
|
||||
</button>
|
||||
<div class="txn-header-bar__title-group">
|
||||
<h3 class="txn-header-bar__title">
|
||||
<fa-icon icon="receipt" class="m-r-8"></fa-icon>
|
||||
Transaction Detail
|
||||
</h3>
|
||||
@if (transactionId) {
|
||||
<span class="txn-header-bar__txn-id">ID: {{ transactionId }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (isViewTransaction()) {
|
||||
<div class="container m-b-20">
|
||||
@if (dataSource) {
|
||||
<mat-card class="mat-elevation-z8" ngClass="{{ journalEntryColor() }}">
|
||||
<mat-card-content>
|
||||
<div class="layout-row-wrap">
|
||||
<div class="flex-25 header">
|
||||
{{ 'labels.inputs.Office' | translate }}
|
||||
</div>
|
||||
<div class="flex-25">
|
||||
{{ dataSource.data[0].officeName }}
|
||||
</div>
|
||||
<div class="flex-25 header">
|
||||
{{ 'labels.inputs.Transaction Date' | translate }}
|
||||
</div>
|
||||
<div class="flex-25">
|
||||
{{ dataSource.data[0].transactionDate | dateFormat }}
|
||||
</div>
|
||||
<div class="flex-25 header">
|
||||
{{ 'labels.inputs.Created By' | translate }}
|
||||
</div>
|
||||
<div class="flex-25">
|
||||
{{ dataSource.data[0].createdByUserName }}
|
||||
</div>
|
||||
<div class="flex-25 header">
|
||||
{{ 'labels.inputs.Submitted on' | translate }}
|
||||
</div>
|
||||
<div class="flex-25">
|
||||
{{ dataSource.data[0].submittedOnDate | datetimeFormat }}
|
||||
</div>
|
||||
<div class="flex-25 header">
|
||||
{{ 'labels.inputs.Manual Journal Entry' | translate }}
|
||||
</div>
|
||||
<div class="flex-25">
|
||||
{{ dataSource.data[0].manualEntry | yesNo }}
|
||||
</div>
|
||||
@if (dataSource.data[0].externalAssetOwner) {
|
||||
<div class="flex-25 header">
|
||||
{{ 'labels.inputs.External Asset Owner' | translate }}
|
||||
</div>
|
||||
}
|
||||
@if (dataSource.data[0].externalAssetOwner) {
|
||||
<div class="flex-25">
|
||||
{{ dataSource.data[0].externalAssetOwner }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div class="txn-header-bar__actions">
|
||||
@if (dataSource?.data[0].manualEntry) {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="warn"
|
||||
(click)="revertTransaction()"
|
||||
*mifosxHasPermission="'REVERSE_JOURNALENTRY'"
|
||||
[disabled]="dataSource?.data[0].reversed"
|
||||
class="txn-revert-btn"
|
||||
>
|
||||
@if (!dataSource.data[0].reversed) {
|
||||
<span><fa-icon icon="undo" class="m-r-6"></fa-icon>Revert Transaction</span>
|
||||
} @else {
|
||||
<span><fa-icon icon="check-circle" class="m-r-6"></fa-icon>Reverted</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Info Cards -->
|
||||
@if (isViewTransaction()) {
|
||||
@if (dataSource) {
|
||||
<div class="txn-info-grid">
|
||||
<div
|
||||
class="txn-info-card"
|
||||
[ngClass]="dataSource.data[0].manualEntry ? 'txn-info-card--manual' : 'txn-info-card--system'"
|
||||
>
|
||||
<div class="txn-info-card__icon">
|
||||
<fa-icon [icon]="dataSource.data[0].manualEntry ? 'pencil-alt' : 'cog'"></fa-icon>
|
||||
</div>
|
||||
<div class="txn-info-card__body">
|
||||
<div class="txn-info-card__label">Entry Type</div>
|
||||
<div class="txn-info-card__value">
|
||||
{{ dataSource.data[0].manualEntry ? 'Manual Entry' : 'System Entry' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="txn-info-card txn-info-card--blue">
|
||||
<div class="txn-info-card__icon"><fa-icon icon="building"></fa-icon></div>
|
||||
<div class="txn-info-card__body">
|
||||
<div class="txn-info-card__label">Office</div>
|
||||
<div class="txn-info-card__value">{{ dataSource.data[0].officeName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="txn-info-card txn-info-card--teal">
|
||||
<div class="txn-info-card__icon"><fa-icon icon="calendar-alt"></fa-icon></div>
|
||||
<div class="txn-info-card__body">
|
||||
<div class="txn-info-card__label">Transaction Date</div>
|
||||
<div class="txn-info-card__value">{{ dataSource.data[0].transactionDate | dateFormat }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="txn-info-card txn-info-card--purple">
|
||||
<div class="txn-info-card__icon"><fa-icon icon="clock"></fa-icon></div>
|
||||
<div class="txn-info-card__body">
|
||||
<div class="txn-info-card__label">Submitted On</div>
|
||||
<div class="txn-info-card__value txn-info-card__value--small">
|
||||
{{ dataSource.data[0].submittedOnDate | datetimeFormat }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="txn-info-card txn-info-card--grey">
|
||||
<div class="txn-info-card__icon"><fa-icon icon="user"></fa-icon></div>
|
||||
<div class="txn-info-card__body">
|
||||
<div class="txn-info-card__label">Created By</div>
|
||||
<div class="txn-info-card__value">{{ dataSource.data[0].createdByUserName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (dataSource.data[0].externalAssetOwner) {
|
||||
<div class="txn-info-card txn-info-card--orange">
|
||||
<div class="txn-info-card__icon"><fa-icon icon="link"></fa-icon></div>
|
||||
<div class="txn-info-card__body">
|
||||
<div class="txn-info-card__label">External Asset Owner</div>
|
||||
<div class="txn-info-card__value">{{ dataSource.data[0].externalAssetOwner }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- DR/CR Summary Bar -->
|
||||
<div class="txn-summary-bar">
|
||||
<div class="txn-summary-bar__item txn-summary-bar__item--dr">
|
||||
<fa-icon icon="arrow-up" class="m-r-6"></fa-icon>
|
||||
Total Debit: <strong>{{ getTotalDebit() | formatNumber }}</strong>
|
||||
</div>
|
||||
<div class="txn-summary-bar__divider"></div>
|
||||
<div class="txn-summary-bar__item txn-summary-bar__item--cr">
|
||||
<fa-icon icon="arrow-down" class="m-r-6"></fa-icon>
|
||||
Total Credit: <strong>{{ getTotalCredit() | formatNumber }}</strong>
|
||||
</div>
|
||||
<div class="txn-summary-bar__divider"></div>
|
||||
<div
|
||||
class="txn-summary-bar__item"
|
||||
[ngClass]="isBalanced() ? 'txn-summary-bar__item--balanced' : 'txn-summary-bar__item--unbalanced'"
|
||||
>
|
||||
<fa-icon [icon]="isBalanced() ? 'check-circle' : 'exclamation-triangle'" class="m-r-6"></fa-icon>
|
||||
{{ isBalanced() ? 'Balanced' : 'Unbalanced' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="mat-elevation-z8 container layout-align-center layout-row-wrap">
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
|
||||
{{ 'labels.inputs.Entry ID' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let transaction">{{ transaction.id }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glAccountType">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
|
||||
{{ 'labels.inputs.Type' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let transaction">{{ transaction.glAccountType.value }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glAccountCode">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
|
||||
{{ 'labels.inputs.Account Code' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let transaction">{{ transaction.glAccountCode }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="glAccountName">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
|
||||
{{ 'labels.inputs.Account Name' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let transaction">{{ transaction.glAccountName }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="debit">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
|
||||
{{ 'labels.inputs.Debit' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let transaction">
|
||||
@if (transaction.entryType.value === 'DEBIT') {
|
||||
<span>
|
||||
{{ transaction.currency.displaySymbol || transaction.currency.code }}
|
||||
{{ transaction.amount | formatNumber }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="credit">
|
||||
<th mat-header-cell class="center" *matHeaderCellDef mat-sort-header>
|
||||
{{ 'labels.inputs.Credit' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let transaction">
|
||||
@if (transaction.entryType.value === 'CREDIT') {
|
||||
<span>
|
||||
{{ transaction.currency.displaySymbol || transaction.currency.code }}
|
||||
{{ transaction.amount | formatNumber }}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="viewJournalEntry(row)"
|
||||
class="select-row"
|
||||
></tr>
|
||||
</table>
|
||||
<div class="layout-row layout-align-center m-t-15 m-b-15 gap-5px layout-xs-column">
|
||||
<button type="button" color="primary" mat-raised-button (click)="goBack()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Journal Entries Table -->
|
||||
<mat-card class="txn-entries-card mat-elevation-z2">
|
||||
<mat-card-header class="txn-entries-card__header">
|
||||
<mat-card-title>
|
||||
<fa-icon icon="table" class="m-r-6"></fa-icon>
|
||||
Journal Entries
|
||||
<span class="txn-entries-count">{{ dataSource?.data?.length || 0 }} entries</span>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="txn-entries-card__content">
|
||||
<div class="txn-table-wrapper">
|
||||
<table mat-table [dataSource]="dataSource" matSort class="txn-table">
|
||||
<!-- Entry ID -->
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header class="txn-th"># ID</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td">
|
||||
<span class="txn-entry-id">{{ entry.id }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Account Type -->
|
||||
<ng-container matColumnDef="glAccountType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header class="txn-th">Account Type</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td">
|
||||
<span class="txn-acct-type-badge" [ngClass]="getAccountTypeBadgeClass(entry.glAccountType?.value)">
|
||||
{{ entry.glAccountType?.value }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Account Code -->
|
||||
<ng-container matColumnDef="glAccountCode">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header class="txn-th">Code</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td">
|
||||
<span class="txn-acct-code">{{ entry.glAccountCode }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Account Name -->
|
||||
<ng-container matColumnDef="glAccountName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header class="txn-th">Account Name</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td">
|
||||
<span class="txn-acct-name">{{ entry.glAccountName }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- DR/CR Badge -->
|
||||
<ng-container matColumnDef="entryType">
|
||||
<th mat-header-cell *matHeaderCellDef class="txn-th txn-th--center">DR / CR</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td txn-td--center">
|
||||
<span
|
||||
class="txn-drcr-badge"
|
||||
[ngClass]="entry.entryType?.value === 'DEBIT' ? 'txn-drcr-badge--dr' : 'txn-drcr-badge--cr'"
|
||||
>
|
||||
{{ entry.entryType?.value === 'DEBIT' ? 'DR' : 'CR' }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Debit -->
|
||||
<ng-container matColumnDef="debit">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header class="txn-th txn-th--amount txn-th--dr">
|
||||
Debit (Dr)
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td txn-td--amount">
|
||||
@if (entry.entryType?.value === 'DEBIT') {
|
||||
<span class="txn-amount txn-amount--dr">
|
||||
{{ entry.currency?.displaySymbol || entry.currency?.code }}
|
||||
{{ entry.amount | formatNumber }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="txn-amount--empty">—</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Credit -->
|
||||
<ng-container matColumnDef="credit">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header class="txn-th txn-th--amount txn-th--cr">
|
||||
Credit (Cr)
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let entry" class="txn-td txn-td--amount">
|
||||
@if (entry.entryType?.value === 'CREDIT') {
|
||||
<span class="txn-amount txn-amount--cr">
|
||||
{{ entry.currency?.displaySymbol || entry.currency?.code }}
|
||||
{{ entry.amount | formatNumber }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="txn-amount--empty">—</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns" class="txn-header-row"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="viewJournalEntry(row)"
|
||||
class="txn-data-row"
|
||||
[ngClass]="row.entryType?.value === 'DEBIT' ? 'txn-row--dr' : 'txn-row--cr'"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="txn-footer-actions">
|
||||
<button mat-stroked-button color="primary" (click)="goBack()">
|
||||
<fa-icon icon="arrow-left" class="m-r-6"></fa-icon> Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isJournalEntryLoaded) {
|
||||
<div class="container m-b-20">
|
||||
<div class="alert">
|
||||
<div class="message">
|
||||
<i class="fa fa-exclamation-circle alert-check"></i>
|
||||
{{ 'labels.text.No data found' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-row layout-align-center m-t-15 m-b-15 gap-5px layout-xs-column">
|
||||
<button type="button" color="primary" mat-raised-button (click)="goBack()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
<div class="txn-no-data">
|
||||
<div class="txn-no-data__box">
|
||||
<fa-icon icon="search" size="2x" class="txn-no-data__icon"></fa-icon>
|
||||
<h4>No Journal Entry Found</h4>
|
||||
<p>The transaction you are looking for does not exist or has been removed.</p>
|
||||
<button mat-raised-button color="primary" (click)="goBack()">
|
||||
<fa-icon icon="arrow-left" class="m-r-6"></fa-icon> Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+487
-5
@@ -6,13 +6,495 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use 'assets/styles/helper';
|
||||
@use 'assets/styles/colours' as *;
|
||||
$dr-color: #e53935;
|
||||
$cr-color: #43a047;
|
||||
$radius: 10px;
|
||||
$shadow: 0 2px 12px rgb(0 0 0 / 8%);
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.select-row:hover {
|
||||
.m-r-8 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.m-r-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
// ─── Page Layout ─────────────────────────────────────────────────────────────
|
||||
.txn-detail-page {
|
||||
padding: 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1rem;
|
||||
}
|
||||
|
||||
// ─── Header Bar ───────────────────────────────────────────────────────────────
|
||||
.txn-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
&__title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-text, #212121);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__txn-id {
|
||||
font-size: 0.75rem;
|
||||
color: #78909c;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.txn-back-btn {
|
||||
color: #546e7a;
|
||||
|
||||
&:hover {
|
||||
color: #1976d2;
|
||||
}
|
||||
}
|
||||
|
||||
.txn-revert-btn {
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
// ─── Info Cards Grid ──────────────────────────────────────────────────────────
|
||||
.txn-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.txn-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-radius: $radius;
|
||||
background: #fff;
|
||||
box-shadow: $shadow;
|
||||
border-left: 4px solid #e0e0e0;
|
||||
transition: transform 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
background: #90a4ae;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #9e9e9e;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #212121;
|
||||
|
||||
&--small {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--manual {
|
||||
border-left-color: #f57f17;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #f57f17, #ffb74d);
|
||||
}
|
||||
}
|
||||
|
||||
&--system {
|
||||
border-left-color: #3949ab;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #3949ab, #7986cb);
|
||||
}
|
||||
}
|
||||
|
||||
&--blue {
|
||||
border-left-color: #1976d2;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #1976d2, #42a5f5);
|
||||
}
|
||||
}
|
||||
|
||||
&--teal {
|
||||
border-left-color: #00796b;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #00796b, #4db6ac);
|
||||
}
|
||||
}
|
||||
|
||||
&--purple {
|
||||
border-left-color: #6a1b9a;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #6a1b9a, #ab47bc);
|
||||
}
|
||||
}
|
||||
|
||||
&--grey {
|
||||
border-left-color: #546e7a;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #546e7a, #90a4ae);
|
||||
}
|
||||
}
|
||||
|
||||
&--orange {
|
||||
border-left-color: #e65100;
|
||||
|
||||
.txn-info-card__icon {
|
||||
background: linear-gradient(135deg, #e65100, #ff8a65);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Summary Bar ──────────────────────────────────────────────────────────────
|
||||
.txn-summary-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0.85rem 1.25rem;
|
||||
background: #fff;
|
||||
border-radius: $radius;
|
||||
box-shadow: $shadow;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__item {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
strong {
|
||||
margin-left: 5px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&--dr {
|
||||
color: $dr-color;
|
||||
}
|
||||
|
||||
&--cr {
|
||||
color: $cr-color;
|
||||
}
|
||||
|
||||
&--balanced {
|
||||
color: #2e7d32;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--unbalanced {
|
||||
color: #c62828;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Entries Card ─────────────────────────────────────────────────────────────
|
||||
.txn-entries-card {
|
||||
border-radius: $radius !important;
|
||||
box-shadow: $shadow !important;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
padding: 0.85rem 1.25rem 0 !important;
|
||||
|
||||
mat-card-title {
|
||||
font-size: 0.95rem !important;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--primary-text, #424242);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.txn-entries-count {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 9px;
|
||||
border-radius: 20px;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
// ─── Table ────────────────────────────────────────────────────────────────────
|
||||
.txn-table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.txn-table {
|
||||
width: 100%;
|
||||
|
||||
.txn-header-row {
|
||||
background: #f5f7fa;
|
||||
|
||||
th {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #616161;
|
||||
padding: 0.7rem 1rem;
|
||||
white-space: nowrap;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.txn-data-row {
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
|
||||
td {
|
||||
padding: 0.65rem 1rem;
|
||||
font-size: 0.84rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f8f9ff !important;
|
||||
}
|
||||
|
||||
&.txn-row--dr td:first-child {
|
||||
border-left: 3px solid rgba($dr-color, 0.4);
|
||||
}
|
||||
|
||||
&.txn-row--cr td:first-child {
|
||||
border-left: 3px solid rgba($cr-color, 0.4);
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.txn-th--center,
|
||||
.txn-td--center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.txn-th--amount {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.txn-td--amount {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.txn-th--dr {
|
||||
color: $dr-color !important;
|
||||
}
|
||||
|
||||
.txn-th--cr {
|
||||
color: $cr-color !important;
|
||||
}
|
||||
|
||||
// ─── Cell Elements ────────────────────────────────────────────────────────────
|
||||
.txn-entry-id {
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
color: #1976d2;
|
||||
background: #e3f2fd;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.txn-acct-code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: #455a64;
|
||||
background: #eceff1;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.txn-acct-name {
|
||||
font-size: 0.84rem;
|
||||
font-weight: 500;
|
||||
color: #37474f;
|
||||
}
|
||||
|
||||
// ─── Badges ───────────────────────────────────────────────────────────────────
|
||||
.txn-acct-type-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
&.txn-badge--asset {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
&.txn-badge--liability {
|
||||
background: #f3e5f5;
|
||||
color: #6a1b9a;
|
||||
}
|
||||
|
||||
&.txn-badge--income {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
&.txn-badge--expense {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
&.txn-badge--equity {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
&.txn-badge--default {
|
||||
background: #f5f5f5;
|
||||
color: #616161;
|
||||
}
|
||||
}
|
||||
|
||||
.txn-drcr-badge {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
padding: 3px 11px;
|
||||
border-radius: 20px;
|
||||
letter-spacing: 0.8px;
|
||||
|
||||
&--dr {
|
||||
background: #ffebee;
|
||||
color: $dr-color;
|
||||
border: 1px solid rgba($dr-color, 0.3);
|
||||
}
|
||||
|
||||
&--cr {
|
||||
background: #e8f5e9;
|
||||
color: $cr-color;
|
||||
border: 1px solid rgba($cr-color, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Amounts ──────────────────────────────────────────────────────────────────
|
||||
.txn-amount {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&--dr {
|
||||
color: $dr-color;
|
||||
}
|
||||
|
||||
&--cr {
|
||||
color: $cr-color;
|
||||
}
|
||||
}
|
||||
|
||||
.txn-amount--empty {
|
||||
color: #bdbdbd;
|
||||
}
|
||||
|
||||
// ─── Footer ───────────────────────────────────────────────────────────────────
|
||||
.txn-footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
// ─── No Data ──────────────────────────────────────────────────────────────────
|
||||
.txn-no-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
|
||||
&__box {
|
||||
text-align: center;
|
||||
padding: 2.5rem;
|
||||
border-radius: $radius;
|
||||
background: #fff;
|
||||
box-shadow: $shadow;
|
||||
max-width: 400px;
|
||||
|
||||
h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #424242;
|
||||
margin: 0.75rem 0 0.4rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.85rem;
|
||||
color: #757575;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-33
@@ -27,13 +27,15 @@ import {
|
||||
MatRowDef,
|
||||
MatRow
|
||||
} from '@angular/material/table';
|
||||
import { Location } from '@angular/common';
|
||||
import { Location, NgClass } from '@angular/common';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { DatetimeFormatPipe } from '../../../pipes/datetime-format.pipe';
|
||||
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { YesnoPipe } from '@pipes/yesno.pipe';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-view-journal-entry-transaction',
|
||||
@@ -54,10 +56,13 @@ import { YesnoPipe } from '@pipes/yesno.pipe';
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
NgClass,
|
||||
DateFormatPipe,
|
||||
DatetimeFormatPipe,
|
||||
FormatNumberPipe,
|
||||
YesnoPipe
|
||||
YesnoPipe,
|
||||
MatCardModule,
|
||||
MatTooltipModule
|
||||
]
|
||||
})
|
||||
export class ViewJournalEntryTransactionComponent implements OnInit {
|
||||
@@ -69,34 +74,27 @@ export class ViewJournalEntryTransactionComponent implements OnInit {
|
||||
|
||||
title: string;
|
||||
journalEntriesData: any[];
|
||||
/** Transaction data. */
|
||||
transaction: any;
|
||||
/** Transaction ID. */
|
||||
transactionId: string;
|
||||
/** Columns to be displayed in transaction table. */
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'id',
|
||||
'glAccountType',
|
||||
'glAccountCode',
|
||||
'glAccountName',
|
||||
'entryType',
|
||||
'debit',
|
||||
'credit'
|
||||
];
|
||||
/** Data source for transaction table. */
|
||||
|
||||
dataSource: MatTableDataSource<any>;
|
||||
|
||||
/** Paginator for transaction table. */
|
||||
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||
/** Sorter for transaction table. */
|
||||
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||
|
||||
isJournalEntryLoaded = false;
|
||||
|
||||
isManualJournalEntry = false;
|
||||
|
||||
/**
|
||||
* Retrieves the transaction data from `resolve` and sets the transaction table.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((data: { title: string; transaction: any; transferJournalEntryData: any }) => {
|
||||
this.title = data.title;
|
||||
@@ -119,14 +117,10 @@ export class ViewJournalEntryTransactionComponent implements OnInit {
|
||||
isViewTransaction(): boolean {
|
||||
return this.title === 'View Transaction';
|
||||
}
|
||||
|
||||
isViewTransfer(): boolean {
|
||||
return this.title === 'View Transfer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the data source for transaction table with journal entries, paginator and sorter.
|
||||
*/
|
||||
setTransaction() {
|
||||
if (this.journalEntriesData != null) {
|
||||
this.dataSource = new MatTableDataSource(this.journalEntriesData);
|
||||
@@ -149,23 +143,13 @@ export class ViewJournalEntryTransactionComponent implements OnInit {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* View details of selected journal entry.
|
||||
* @param {any} journalEntry Selected journal entry.
|
||||
*/
|
||||
viewJournalEntry(journalEntry: any) {
|
||||
this.dialog.open(ViewJournalEntryComponent, {
|
||||
data: { journalEntry: journalEntry }
|
||||
});
|
||||
this.dialog.open(ViewJournalEntryComponent, { data: { journalEntry } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts the given transaction and redirects to reverted transaction.
|
||||
* @param {transactionId} transactionId Transaction ID of transaction to be reverted.
|
||||
*/
|
||||
revertTransaction(transactionId?: string) {
|
||||
const revertTransactionDialogRef = this.dialog.open(RevertTransactionComponent, {
|
||||
data: { reverted: this.dataSource.data[0].reversed, transactionId: transactionId }
|
||||
data: { reverted: this.dataSource.data[0].reversed, transactionId }
|
||||
});
|
||||
revertTransactionDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.revert) {
|
||||
@@ -191,10 +175,30 @@ export class ViewJournalEntryTransactionComponent implements OnInit {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
journalEntryColor(): string {
|
||||
if (this.isManualJournalEntry) {
|
||||
return 'manual-entry';
|
||||
}
|
||||
return '';
|
||||
getTotalDebit(): number {
|
||||
return (this.dataSource?.data || [])
|
||||
.filter((e) => e.entryType?.value === 'DEBIT')
|
||||
.reduce((sum, e) => sum + (e.amount || 0), 0);
|
||||
}
|
||||
|
||||
getTotalCredit(): number {
|
||||
return (this.dataSource?.data || [])
|
||||
.filter((e) => e.entryType?.value === 'CREDIT')
|
||||
.reduce((sum, e) => sum + (e.amount || 0), 0);
|
||||
}
|
||||
|
||||
isBalanced(): boolean {
|
||||
return Math.abs(this.getTotalDebit() - this.getTotalCredit()) < 0.01;
|
||||
}
|
||||
|
||||
getAccountTypeBadgeClass(type: string): string {
|
||||
const map: Record<string, string> = {
|
||||
ASSET: 'txn-badge--asset',
|
||||
LIABILITY: 'txn-badge--liability',
|
||||
INCOME: 'txn-badge--income',
|
||||
EXPENSE: 'txn-badge--expense',
|
||||
EQUITY: 'txn-badge--equity'
|
||||
};
|
||||
return map[type] || 'txn-badge--default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,25 @@ import {
|
||||
faRobot,
|
||||
faMicrochip,
|
||||
faMagic,
|
||||
faQrcode
|
||||
faQrcode,
|
||||
faCalendarDay,
|
||||
faCalendarWeek,
|
||||
faChartArea,
|
||||
faChevronUp,
|
||||
faWallet,
|
||||
faExchangeAlt,
|
||||
faArrowsAltH,
|
||||
faSignInAlt,
|
||||
faDoorOpen,
|
||||
faBoxOpen,
|
||||
faToggleOn,
|
||||
faToggleOff,
|
||||
faChartColumn,
|
||||
faMoneyBillTrendUp,
|
||||
faScaleBalanced,
|
||||
faNotesMedical,
|
||||
faFileLines,
|
||||
faBuildingColumns
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
/**
|
||||
@@ -393,7 +411,25 @@ export class IconsModule {
|
||||
faRobot,
|
||||
faMicrochip,
|
||||
faMagic,
|
||||
faQrcode
|
||||
faQrcode,
|
||||
faCalendarDay,
|
||||
faCalendarWeek,
|
||||
faChartArea,
|
||||
faChevronUp,
|
||||
faWallet,
|
||||
faExchangeAlt,
|
||||
faArrowsAltH,
|
||||
faSignInAlt,
|
||||
faDoorOpen,
|
||||
faBoxOpen,
|
||||
faToggleOn,
|
||||
faToggleOff,
|
||||
faChartColumn,
|
||||
faMoneyBillTrendUp,
|
||||
faScaleBalanced,
|
||||
faNotesMedical,
|
||||
faFileLines,
|
||||
faBuildingColumns
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user