- Add Balance Sheet report and disabled-features accounting config - Point menu-access tenant header and env.js/environment.ts tenant identifier at env config instead of hardcoded 'default'/'lincpay' literals - Surface the real backend error (not a generic "no reference id") when an OTP send fails in contact-otp-block - Fix branch-day "₹null" display bug (null vs undefined) and add a guard blocking a branch's first-ever day open before cash denomination migration - Dynamically size the denomination count input and block re-opening an already-open business day from Migrate Cash Denomination - Lock the denomination-managed cash GL row in Migrate Opening Balances (read-only, with a link back to Migrate Cash Denomination) - Remove duplicate Cancel/Submit action bars on the Share Product and Savings Account creation preview steps; align Previous into the same row - Hide "Apply Collection" on a share account once its advance collection is already settled (further purchases go through Apply Additional Shares) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
629 lines
22 KiB
TypeScript
629 lines
22 KiB
TypeScript
/**
|
|
* Copyright since 2025 Mifos Initiative
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
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 } from '@angular/forms';
|
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
import { merge } from 'rxjs';
|
|
import { tap, startWith, map, distinctUntilChanged, debounceTime } from 'rxjs/operators';
|
|
import { AccountingService } from '../accounting.service';
|
|
import { DISABLED_ACCOUNTING_FEATURES } from '../disabled-features';
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { JournalEntriesDataSource } from './journal-entry.datasource';
|
|
import { Dates } from 'app/core/utils/dates';
|
|
import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
|
|
import { GlAccountSelectorComponent } from '../../shared/accounting/gl-account-selector/gl-account-selector.component';
|
|
import { AsyncPipe, NgClass, NgIf, NgFor, DecimalPipe, CurrencyPipe } from '@angular/common';
|
|
import {
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
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';
|
|
|
|
@Component({
|
|
selector: 'mifosx-search-journal-entry',
|
|
standalone: true,
|
|
templateUrl: './search-journal-entry.component.html',
|
|
styleUrls: ['./search-journal-entry.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatAutocompleteTrigger,
|
|
GlAccountSelectorComponent,
|
|
MatAutocomplete,
|
|
MatTable,
|
|
MatSort,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatSortHeader,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow,
|
|
MatNoDataRow,
|
|
MatPaginator,
|
|
AsyncPipe,
|
|
NgClass,
|
|
NgIf,
|
|
NgFor,
|
|
DateFormatPipe,
|
|
DatetimeFormatPipe,
|
|
FormatNumberPipe,
|
|
FaIconComponent,
|
|
MatCardModule,
|
|
MatTooltipModule,
|
|
MatIconModule,
|
|
RouterLink
|
|
]
|
|
})
|
|
export class SearchJournalEntryComponent implements OnInit, OnDestroy, AfterViewInit {
|
|
private accountingService = inject(AccountingService);
|
|
private settingsService = inject(SettingsService);
|
|
private dateUtils = inject(Dates);
|
|
private route = inject(ActivatedRoute);
|
|
|
|
// ── Misc ──────────────────────────────────────────────────────────────────
|
|
minDate = new Date(2000, 0, 1);
|
|
maxDate = new Date();
|
|
/** Accounting features switched off for this installation — see disabled-features.ts. */
|
|
disabledFeatures = DISABLED_ACCOUNTING_FEATURES;
|
|
|
|
// ── Filter controls ───────────────────────────────────────────────────────
|
|
officeName = new UntypedFormControl();
|
|
officeData: any;
|
|
filteredOfficeData: any;
|
|
glAccount = new UntypedFormControl();
|
|
glAccountData: any;
|
|
entryTypeFilter = new UntypedFormControl('');
|
|
transactionDateFrom = new UntypedFormControl(new Date(new Date().setMonth(new Date().getMonth() - 1)));
|
|
transactionDateTo = new UntypedFormControl(new Date());
|
|
transactionIdCtrl = new UntypedFormControl();
|
|
submittedOnDateFrom = new UntypedFormControl();
|
|
submittedOnDateTo = new UntypedFormControl();
|
|
referenceNumber = new UntypedFormControl();
|
|
|
|
entryTypeFilterData = [
|
|
{ option: 'All Entries', value: '' },
|
|
{ option: 'Manual Entries', value: 'true' },
|
|
{ option: 'System Entries', value: 'false' }
|
|
];
|
|
|
|
// ── Table columns ─────────────────────────────────────────────────────────
|
|
displayedColumns: string[] = [
|
|
'id',
|
|
'transactionDate',
|
|
'transactionId',
|
|
'savingsAccountNo',
|
|
'officeName',
|
|
'glAccountCode',
|
|
'glAccountName',
|
|
'glAccountType',
|
|
'entryType',
|
|
'debit',
|
|
'credit',
|
|
'runningBalance',
|
|
'manualEntry',
|
|
'createdByUserName',
|
|
'submittedOnDate',
|
|
'actions'
|
|
];
|
|
dayWiseColumns = [
|
|
'date',
|
|
'openingBalance',
|
|
'totalDebit',
|
|
'totalCredit',
|
|
'netMovement',
|
|
'closingBalance',
|
|
'entryCount'
|
|
];
|
|
|
|
dataSource: JournalEntriesDataSource;
|
|
|
|
// ── 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: 'fromDate',
|
|
value: this.dateUtils.formatDate(
|
|
new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
|
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 }
|
|
];
|
|
|
|
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
|
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
|
|
|
constructor() {
|
|
this.route.data.subscribe((data: { offices: any; glAccounts: any }) => {
|
|
this.officeData = data.offices;
|
|
this.glAccountData = data.glAccounts;
|
|
});
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.maxDate = this.settingsService.businessDate;
|
|
this.paginator.pageSize = 10;
|
|
this.restoreState();
|
|
this.setFilteredOffices();
|
|
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 */
|
|
}
|
|
}
|
|
|
|
ngAfterViewInit() {
|
|
this.officeName.valueChanges
|
|
.pipe(
|
|
map((v) => (v?.id ? String(v.id) : '')),
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
tap((val) => this.applyFilter(val, 'officeId'))
|
|
)
|
|
.subscribe();
|
|
|
|
this.glAccount.valueChanges
|
|
.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
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.entryTypeFilter.valueChanges
|
|
.pipe(
|
|
debounceTime(300),
|
|
distinctUntilChanged(),
|
|
tap((val) => this.applyFilter(val ?? '', 'manualEntriesOnly'))
|
|
)
|
|
.subscribe();
|
|
|
|
this.transactionIdCtrl.valueChanges
|
|
.pipe(
|
|
debounceTime(600),
|
|
distinctUntilChanged(),
|
|
tap((val) => this.applyFilter(val ?? '', 'transactionId'))
|
|
)
|
|
.subscribe();
|
|
|
|
this.transactionDateFrom.valueChanges
|
|
.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
tap((val) => this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'fromDate'))
|
|
)
|
|
.subscribe();
|
|
|
|
this.transactionDateTo.valueChanges
|
|
.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
tap((val) => this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'toDate'))
|
|
)
|
|
.subscribe();
|
|
|
|
this.submittedOnDateFrom.valueChanges
|
|
.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
tap((val) =>
|
|
this.applyFilter(this.dateUtils.formatDate(val, this.settingsService.dateFormat), 'submittedOnDateFrom')
|
|
)
|
|
)
|
|
.subscribe();
|
|
|
|
this.submittedOnDateTo.valueChanges
|
|
.pipe(
|
|
debounceTime(500),
|
|
distinctUntilChanged(),
|
|
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();
|
|
}
|
|
|
|
// ── 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 as any).active;
|
|
}
|
|
this.dataSource.getJournalEntries(
|
|
this.filterJournalEntriesBy,
|
|
this.sort.active,
|
|
this.sort.direction,
|
|
this.paginator.pageIndex,
|
|
this.paginator.pageSize || 10
|
|
);
|
|
}
|
|
|
|
applyFilter(filterValue: string, property: string) {
|
|
this.paginator.pageIndex = 0;
|
|
const idx = this.filterJournalEntriesBy.findIndex((f) => f.type === property);
|
|
if (idx >= 0) this.filterJournalEntriesBy[idx].value = filterValue;
|
|
this.loadJournalEntriesPage();
|
|
this.loadLedgerSummary();
|
|
}
|
|
|
|
getJournalEntries() {
|
|
this.dataSource = new JournalEntriesDataSource(this.accountingService);
|
|
this.dataSource.getJournalEntries(
|
|
this.filterJournalEntriesBy,
|
|
this.sort.active,
|
|
this.sort.direction,
|
|
this.paginator.pageIndex,
|
|
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);
|
|
}
|
|
}
|