/** * 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. * *

* 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(); }