- 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>
186 lines
6.8 KiB
TypeScript
186 lines
6.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/.
|
||
*/
|
||
|
||
/**
|
||
* 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();
|
||
}
|