- 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>
198 lines
4.8 KiB
TypeScript
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
|
|
);
|
|
}
|
|
}
|