Files
cbsnexora/src/app/shares/reports/share-report.util.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

186 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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/.
*/
/**
* Shared vocabulary and plumbing for the share reports.
*
* Kept in one place because the reports are read side by side — a member shown as "Active" on the
* balance report and "Approved" on the status report would read as two different things when it is
* one status_enum, and a CSV that quotes its fields on one screen but not the next breaks whichever
* spreadsheet opens both.
*/
/** m_share_account.status_enum → what a reader calls it, and the pill it wears. */
export const SHARE_ACCOUNT_STATUS: { [key: number]: { label: string; cls: string } } = {
100: { label: 'Pending Approval', cls: 'srep-pill--amber' },
200: { label: 'Approved', cls: 'srep-pill--blue' },
300: { label: 'Active', cls: 'srep-pill--green' },
500: { label: 'Rejected', cls: 'srep-pill--red' },
600: { label: 'Closed', cls: 'srep-pill--grey' }
};
/**
* Why a share account's holding reads the way it does — the server's own derivation, so every
* report tells the member the same story about the same account.
*
* <p>
* A nil holding is the most over-loaded figure on a share register. A closed account, an active
* account whose money was never collected, and an account redeemed down to nothing all print the
* same "0", and a reader cannot tell them apart. Each state therefore carries its own pill, its own
* row treatment, and a sentence saying what it means — closed and finished rows are dimmed, and the
* one state that still wants chasing is flagged rather than greyed out.
*/
export const SHARE_HOLDING_STATE: {
[key: string]: { label: string; cls: string; row: string; note: string };
} = {
HOLDING: {
label: 'Holding',
cls: 'srep-pill--green',
row: '',
note: 'Paid-up shares in hand.'
},
UNPAID: {
label: 'Allotted · unpaid',
cls: 'srep-pill--amber',
row: 'srep-row--flag',
note: 'Shares allotted, but the collection was never applied — so they are in no capital figure.'
},
NIL: {
label: 'Nil holding',
cls: 'srep-pill--grey',
row: 'srep-row--muted',
note: 'Active and paid for, but redeemed back down to nothing.'
},
CLOSED: {
label: 'Closed',
cls: 'srep-pill--grey',
row: 'srep-row--muted',
note: 'The account is closed — the nil holding is final and correct.'
},
NOT_ACTIVE: {
label: 'Not active',
cls: 'srep-pill--blue',
row: 'srep-row--muted',
note: 'Submitted or approved, not yet activated.'
},
REJECTED: {
label: 'Rejected',
cls: 'srep-pill--red',
row: 'srep-row--muted',
note: 'The application never became an account.'
}
};
/** The states in the order a reader thinks about them — live first, finished last. */
export const SHARE_HOLDING_STATE_ORDER = [
'HOLDING',
'UNPAID',
'NIL',
'CLOSED',
'NOT_ACTIVE',
'REJECTED'
];
export function holdingStateLabel(state: string | null | undefined): string {
return SHARE_HOLDING_STATE[state || '']?.label || state || '—';
}
export function holdingStateClass(state: string | null | undefined): string {
return SHARE_HOLDING_STATE[state || '']?.cls || 'srep-pill--grey';
}
/** The row treatment: dimmed when the account is over, flagged when it still wants chasing. */
export function holdingStateRowClass(state: string | null | undefined): string {
return SHARE_HOLDING_STATE[state || '']?.row || '';
}
export function holdingStateNote(state: string | null | undefined): string {
return SHARE_HOLDING_STATE[state || '']?.note || '';
}
/** m_share_account_transactions.type_enum — only the two that move shares. */
export const SHARE_TXN_TYPE: { [key: number]: { label: string; cls: string } } = {
500: { label: 'Purchase', cls: 'srep-pill--green' },
600: { label: 'Redemption', cls: 'srep-pill--red' },
700: { label: 'Charge Payment', cls: 'srep-pill--grey' }
};
/** m_share_account_transactions.status_enum — a tranche's own approval state. */
export const SHARE_TXN_STATUS: { [key: number]: { label: string; cls: string } } = {
100: { label: 'Pending', cls: 'srep-pill--amber' },
300: { label: 'Approved', cls: 'srep-pill--green' },
400: { label: 'Rejected', cls: 'srep-pill--red' }
};
/** How a tranche was paid for; blank on the opening tranche, which the account itself carries. */
export function fundingLabel(source: string | null | undefined): string {
if (!source) {
return 'Opening';
}
return source === 'CASH' ? 'Cash Receipt' : source === 'SAVINGS' ? 'Savings Debit' : source;
}
/** m_share_account_dividend_details.status — where a member's dividend has got to. */
export const SHARE_DIVIDEND_STATUS: { [key: number]: { label: string; cls: string } } = {
100: { label: 'Declared', cls: 'srep-pill--amber' },
300: { label: 'Approved', cls: 'srep-pill--blue' },
600: { label: 'Paid', cls: 'srep-pill--green' }
};
/** Where a transfer stands in maker-checker. */
export const SHARE_TRANSFER_STATUS: { [key: string]: { label: string; cls: string } } = {
PENDING: { label: 'Pending approval', cls: 'srep-pill--amber' },
APPROVED: { label: 'Approved', cls: 'srep-pill--green' },
REJECTED: { label: 'Rejected', cls: 'srep-pill--red' }
};
/**
* Fineract's period frequency enum, as a phrase.
*
* Reads "18 months" rather than "18 / 2", which is what the raw pair means to nobody outside the
* schema. An unset period says "None" rather than showing a bare zero that looks like a real term.
*/
export function periodLabel(value: number | null | undefined, frequencyEnum: number | null | undefined): string {
if (!value) {
return 'None';
}
const unit = { 0: 'day', 1: 'week', 2: 'month', 3: 'year' }[frequencyEnum ?? -1] || 'period';
return `${value} ${unit}${Number(value) === 1 ? '' : 's'}`;
}
/**
* Downloads rows as CSV.
*
* Every field is quoted and its own quotes doubled, so a member name carrying a comma — which is
* most of them, once an address or a title creeps in — cannot shift every later column by one.
*/
export function shareReportCsv(
filename: string,
headers: string[],
rows: (string | number | null | undefined)[][]
): void {
const escape = (value: string | number | null | undefined): string => `"${String(value ?? '').replace(/"/g, '""')}"`;
const csv = [
headers.map(escape).join(','),
...rows.map((row) => row.map(escape).join(','))
].join('\n');
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename}.csv`;
link.click();
URL.revokeObjectURL(url);
}
/** The browser's own print, against the component's print stylesheet. */
export function shareReportPrint(): void {
window.print();
}