Files
cbsnexora/src/app/shares/reports/share-label/share-label.component.ts
T
niteshandClaude Sonnet 5 c7428d0cb2 feat(shares): dynamic Share Reports module, voucher attach/GL sync fixes
- Add the full Share Reports surface (final balance, account statement,
  issued/status/nominal/share-wise/guarantor/member-interest reports, enquiry,
  payment certificate, share detail, dividend statement, folio statement,
  distinctive numbers, area-wise, mailing labels/letters, share transfer) as
  standalone routed components, wired into the Share menu
- Wire the unattributed Share Collection receipt list into Apply Collection
  and Apply Additional Shares, with an Attach action independent of
  account/client
- Sync the GL Account and Voucher No fields to whichever receipt is ticked or
  found, and surface the receipt's real Dr/Cr GL entries in its detail popup
- Block Apply Additional Shares until the account's original application is
  settled

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:06:02 +05:30

198 lines
4.8 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, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { SharesService } from '../../shares.service';
import { shareReportCsv, shareReportPrint } from '../share-report.util';
export interface MailingRow {
clientId: number;
folioNumber: string;
clientName: string;
mobileNo: string;
officeName: string;
addressLine1: string;
addressLine2: string;
street: string;
city: string;
district: string;
postalCode: string;
area: string;
sharesHeld: number;
shareValue: number;
shareAccountNos: string;
addressable: boolean;
}
/** Assembles one member's address into the lines an envelope actually carries. */
export function addressLines(row: MailingRow): string[] {
return [
row.addressLine1,
row.addressLine2,
row.street,
[
row.city,
row.district
]
.filter(Boolean)
.join(', '),
row.postalCode ? `PIN ${row.postalCode}` : ''
]
.map((line) => (line || '').trim())
.filter(Boolean);
}
/**
* Share Label — printable address labels for shareholding members.
*
* <p>
* Laid out as a grid sized to ordinary label sheets and printed straight from the browser, so a
* mailing run needs no separate mail-merge tool. Members with no usable address are listed
* separately rather than printed as a blank label: a blank label is a wasted sheet and a member who
* silently never hears from the society.
*/
@Component({
selector: 'mifosx-share-label',
templateUrl: './share-label.component.html',
styleUrls: [
'../../../letter-mgmt/certificates/certificates.shared.scss',
'../share-report.shared.scss'
],
imports: [
CommonModule,
FormsModule,
FaIconComponent
]
})
export class ShareLabelComponent implements OnInit {
private sharesService = inject(SharesService);
loading = true;
loadError = '';
rows: MailingRow[] = [];
totals: any = null;
search = '';
areaFilter = '';
/** Labels per row on the sheet — 2 and 3 cover the sheets a branch actually buys. */
columns = 3;
showFolio = true;
showHolding = false;
ngOnInit(): void {
this.load();
}
load(): void {
this.loading = true;
this.loadError = '';
this.sharesService
.getShareMailingList({})
.pipe(
catchError((err) => {
this.loadError = this.readError(err, 'The mailing list could not be loaded.');
return of(null);
})
)
.subscribe((res: any) => {
this.rows = res?.rows || [];
this.totals = res?.totals || null;
this.loading = false;
});
}
get areaOptions(): string[] {
return [...new Set(this.rows.map((r) => r.area).filter(Boolean))].sort();
}
/** Only members who can actually be posted to — the rest are listed below, not printed. */
get printableRows(): MailingRow[] {
return this.filteredRows.filter((r) => r.addressable);
}
get unaddressableRows(): MailingRow[] {
return this.filteredRows.filter((r) => !r.addressable);
}
get filteredRows(): MailingRow[] {
const term = this.search.trim().toLowerCase();
return this.rows.filter((r) => {
if (this.areaFilter && r.area !== this.areaFilter) {
return false;
}
if (!term) {
return true;
}
return [
r.clientName,
r.folioNumber,
r.area,
r.postalCode
]
.filter(Boolean)
.some((field) => String(field).toLowerCase().includes(term));
});
}
lines(row: MailingRow): string[] {
return addressLines(row);
}
clearFilters(): void {
this.search = '';
this.areaFilter = '';
}
exportCsv(): void {
shareReportCsv(
'share-mailing-labels',
[
'Member',
'Folio No',
'Address',
'City',
'District',
'Postal Code',
'Mobile',
'Shares Held',
'Addressable'
],
this.filteredRows.map((r) => [
r.clientName,
r.folioNumber,
this.lines(r).join(' / '),
r.city,
r.district,
r.postalCode,
r.mobileNo,
r.sharesHeld,
r.addressable ? 'Yes' : 'No'
])
);
}
print(): void {
shareReportPrint();
}
private readError(err: any, fallback: string): string {
return (
err?.error?.errors?.[0]?.defaultUserMessage ||
err?.error?.defaultUserMessage ||
err?.error?.developerMessage ||
fallback
);
}
}