feat(shares): one-voucher-per-slot receipt dialogs, Apply Additional Shares as a popup
Apply Collection and Apply Additional Shares settle the same Share Collection receipts for the same member, so they are now deliberately the same screen. Apply Collection takes exactly two receipts and no more: one for the share value, one for the entry fee, in two separate slots. They are different things -- the share receipt is money moving into share capital, while the fee receipt only proves the fee was already collected and is never posted a second time -- and picking both out of one ticked list made that distinction invisible. Apply Additional Shares moves from its own route to a dialog. It was a full page a clerk had to leave the account for and come back from, to fill in a short form with the member standing at the counter. Its old route, resolver case and action- host branch are removed rather than left behind as dead paths. Both screens now draw their receipts from a single pool, whether a receipt already names the member or is a voucher paid straight into a GL account; an unattributed one is attached at the moment it is picked, so what used to be an Attach in one table and a tick in another is one deliberate click. The by-voucher search and the GL-entry detail both sit behind buttons, as exceptions rather than permanent furniture. Dialog chrome and the receipt shape are shared (share-dialog.shared.scss, share-receipt-option.ts) so the two cannot drift apart again. Also fixes content being clipped at the dialog edges: the wrapper cancelled a 24px surface padding that the panel class had already zeroed, pushing every edge outside the surface where overflow:hidden cut it off, and the height cap sat on the scrolling body rather than the wrapper, letting the footer stack past the bottom of the viewport. Adds the product-wise share summary report (authorised, issued, available and unpaid shares, distinctive range and account count per product). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="cert-root">
|
||||
<div class="cert-head">
|
||||
<div class="cert-head-l">
|
||||
<fa-icon icon="layer-group" class="cert-head-ic"></fa-icon>
|
||||
<div>
|
||||
<h2 class="cert-head-t">Share Product Summary</h2>
|
||||
<div class="cert-head-s">
|
||||
Every share class against its own authorised capital — shares issued, shares remaining, and the
|
||||
distinctive-number range issued so far. "Issued" means paid up, not merely allotted.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-head-r">
|
||||
<button class="cert-cta-btn" (click)="exportCsv()" [disabled]="loading || !rows.length">
|
||||
<fa-icon icon="file-alt"></fa-icon> Export CSV
|
||||
</button>
|
||||
<button class="cert-cta-btn" (click)="print()" [disabled]="loading || !rows.length">
|
||||
<fa-icon icon="print"></fa-icon> Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (rows.length) {
|
||||
<div class="cert-kpis">
|
||||
<div class="cert-kpi cert-kpi--blue">
|
||||
<fa-icon icon="layer-group" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totals.authorisedShares | number }}</div>
|
||||
<div class="cert-kpi-l">Authorised Shares (all classes)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--green">
|
||||
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totals.issuedShares | number }}</div>
|
||||
<div class="cert-kpi-l">Shares Issued (paid up)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi cert-kpi--teal">
|
||||
<fa-icon icon="balance-scale" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">₹{{ totals.issuedCapital | number: '1.2-2' }}</div>
|
||||
<div class="cert-kpi-l">Share Capital Issued</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-kpi" [ngClass]="totals.unpaidShares ? 'cert-kpi--amber' : 'cert-kpi--blue'">
|
||||
<fa-icon icon="exclamation-circle" class="cert-kpi-ic"></fa-icon>
|
||||
<div>
|
||||
<div class="cert-kpi-n">{{ totals.unpaidShares | number }}</div>
|
||||
<div class="cert-kpi-l">Allotted, Unpaid</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
<div class="srep-state">Loading product summary…</div>
|
||||
} @else if (loadError) {
|
||||
<div class="srep-state srep-state--error"><fa-icon icon="exclamation-circle"></fa-icon> {{ loadError }}</div>
|
||||
} @else if (!rows.length) {
|
||||
<div class="srep-state">No share product is configured.</div>
|
||||
} @else {
|
||||
<div class="srep-table-wrap">
|
||||
<table class="srep-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Share Class</th>
|
||||
<th class="srep-num">Unit Price</th>
|
||||
<th class="srep-num">Authorised</th>
|
||||
<th class="srep-num">Issued (Paid Up)</th>
|
||||
<th class="srep-num">Available</th>
|
||||
<th>Utilisation</th>
|
||||
<th class="srep-num">Allotted, Unpaid</th>
|
||||
<th>Distinctive Range</th>
|
||||
<th class="srep-num">Accounts</th>
|
||||
<th>Min / Nominal / Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of rows; track row.productId) {
|
||||
<tr>
|
||||
<td>
|
||||
{{ row.productName }}
|
||||
@if (row.productShortName) {
|
||||
<div class="srep-mono srep-sub" style="color: inherit; opacity: 0.65">{{ row.productShortName }}</div>
|
||||
}
|
||||
</td>
|
||||
<td class="srep-num">{{ row.unitPrice | number: '1.2-2' }}</td>
|
||||
<td class="srep-num">
|
||||
{{ row.authorisedShares | number }}
|
||||
<div class="srep-sub" style="color: inherit; opacity: 0.65">
|
||||
₹{{ row.authorisedCapital | number: '1.0-0' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="srep-num">
|
||||
{{ row.issuedShares | number }}
|
||||
<div class="srep-sub" style="color: inherit; opacity: 0.65">
|
||||
₹{{ row.issuedCapital | number: '1.0-0' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="srep-num">{{ row.availableShares | number }}</td>
|
||||
<td>
|
||||
<div class="product-utilisation-bar">
|
||||
<div class="product-utilisation-fill" [style.width.%]="utilisationPercent(row)"></div>
|
||||
</div>
|
||||
<div class="srep-mono" style="font-size: 0.72rem; color: #64748b">
|
||||
{{ utilisationPercent(row) | number: '1.0-1' }}%
|
||||
</div>
|
||||
</td>
|
||||
<td class="srep-num">
|
||||
@if (row.unpaidShares) {
|
||||
<span class="srep-pill srep-pill--amber">{{ row.unpaidShares | number }}</span>
|
||||
<div class="srep-sub" style="color: inherit; opacity: 0.65">
|
||||
₹{{ row.unpaidAmount | number: '1.0-0' }} · {{ row.unpaidAccounts }}
|
||||
{{ row.unpaidAccounts === 1 ? 'account' : 'accounts' }}
|
||||
</div>
|
||||
} @else {
|
||||
<span class="srep-pill srep-pill--green">0</span>
|
||||
}
|
||||
</td>
|
||||
<td class="srep-mono">{{ distinctiveRange(row) }}</td>
|
||||
<td class="srep-num">
|
||||
{{ row.accountCount | number }}
|
||||
<div class="srep-sub" style="color: inherit; opacity: 0.65">
|
||||
{{ row.memberCount | number }} {{ row.memberCount === 1 ? 'member' : 'members' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="srep-mono">
|
||||
{{ row.minimumClientShares ?? '—' }} / {{ row.nominalClientShares ?? '—' }} /
|
||||
{{ row.maximumClientShares ?? '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Total — {{ rows.length | number }} share classes</td>
|
||||
<td class="srep-num"></td>
|
||||
<td class="srep-num">{{ totals.authorisedShares | number }}</td>
|
||||
<td class="srep-num">{{ totals.issuedShares | number }}</td>
|
||||
<td class="srep-num" colspan="2"></td>
|
||||
<td class="srep-num">{{ totals.unpaidShares | number }}</td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (totals.unpaidShares) {
|
||||
<div class="srep-warn">
|
||||
<fa-icon icon="exclamation-circle"></fa-icon>
|
||||
<span>
|
||||
<b>{{ totals.unpaidShares | number }}</b> shares across every class are allotted but not yet paid for —
|
||||
<b>₹{{ totals.unpaidAmount | number: '1.2-2' }}</b> in all. They are excluded from "Issued" above; see Final
|
||||
Balance for which members they belong to.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 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 { 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';
|
||||
|
||||
interface ProductSummaryRow {
|
||||
productId: number;
|
||||
productName: string;
|
||||
productShortName: string;
|
||||
unitPrice: number;
|
||||
minimumClientShares: number;
|
||||
nominalClientShares: number;
|
||||
maximumClientShares: number;
|
||||
authorisedShares: number;
|
||||
authorisedCapital: number;
|
||||
issuedShares: number;
|
||||
issuedCapital: number;
|
||||
availableShares: number;
|
||||
unpaidAccounts: number;
|
||||
unpaidShares: number;
|
||||
unpaidAmount: number;
|
||||
distinctiveFrom: number | null;
|
||||
distinctiveTo: number | null;
|
||||
accountCount: number;
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Share Product Summary — every share class measured against its own authorised capital: how many
|
||||
* shares are issued, how many remain, and the distinctive-number range issued so far.
|
||||
*
|
||||
* <p>
|
||||
* "Issued" is paid-up shares — settled purchases less redemptions, computed the same way every other
|
||||
* report in this suite computes them — not {@code m_share_product.totalsubscribed_shares}. That
|
||||
* platform column counts an allotment the moment it is submitted, before any money has necessarily
|
||||
* arrived, so it runs ahead of what the society has actually received. A board reading this page is
|
||||
* reading the same figure the GL Share Capital account would show, not a subscription count.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-share-product-summary',
|
||||
templateUrl: './share-product-summary.component.html',
|
||||
styleUrls: [
|
||||
'../../../letter-mgmt/certificates/certificates.shared.scss',
|
||||
'../share-report.shared.scss'
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class ShareProductSummaryComponent implements OnInit {
|
||||
private sharesService = inject(SharesService);
|
||||
|
||||
loading = true;
|
||||
loadError = '';
|
||||
rows: ProductSummaryRow[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.loadError = '';
|
||||
this.sharesService
|
||||
.getShareProductSummary()
|
||||
.pipe(
|
||||
catchError((err: any) => {
|
||||
this.loadError = this.readError(err, 'The product summary could not be loaded.');
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.rows = res?.rows || [];
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** How much of a product's authorised capital is actually issued, for the progress bar. */
|
||||
utilisationPercent(row: ProductSummaryRow): number {
|
||||
if (!row.authorisedShares) {
|
||||
return 0;
|
||||
}
|
||||
const pct = (row.issuedShares / row.authorisedShares) * 100;
|
||||
return Math.max(0, Math.min(100, pct));
|
||||
}
|
||||
|
||||
distinctiveRange(row: ProductSummaryRow): string {
|
||||
if (!row.distinctiveFrom || !row.distinctiveTo) {
|
||||
return 'None allotted yet';
|
||||
}
|
||||
return `${row.distinctiveFrom} – ${row.distinctiveTo}`;
|
||||
}
|
||||
|
||||
get totals() {
|
||||
return {
|
||||
authorisedShares: this.rows.reduce((sum, r) => sum + Number(r.authorisedShares || 0), 0),
|
||||
authorisedCapital: this.rows.reduce((sum, r) => sum + Number(r.authorisedCapital || 0), 0),
|
||||
issuedShares: this.rows.reduce((sum, r) => sum + Number(r.issuedShares || 0), 0),
|
||||
issuedCapital: this.rows.reduce((sum, r) => sum + Number(r.issuedCapital || 0), 0),
|
||||
unpaidShares: this.rows.reduce((sum, r) => sum + Number(r.unpaidShares || 0), 0),
|
||||
unpaidAmount: this.rows.reduce((sum, r) => sum + Number(r.unpaidAmount || 0), 0)
|
||||
};
|
||||
}
|
||||
|
||||
exportCsv(): void {
|
||||
shareReportCsv(
|
||||
'share-product-summary',
|
||||
[
|
||||
'Product',
|
||||
'Unit Price',
|
||||
'Authorised Shares',
|
||||
'Authorised Capital',
|
||||
'Issued Shares',
|
||||
'Issued Capital',
|
||||
'Available Shares',
|
||||
'Unpaid Accounts',
|
||||
'Unpaid Shares',
|
||||
'Unpaid Amount',
|
||||
'Distinctive Range',
|
||||
'Accounts',
|
||||
'Members',
|
||||
'Min / Nominal / Max Per Member'
|
||||
],
|
||||
this.rows.map((r) => [
|
||||
r.productName,
|
||||
r.unitPrice,
|
||||
r.authorisedShares,
|
||||
r.authorisedCapital,
|
||||
r.issuedShares,
|
||||
r.issuedCapital,
|
||||
r.availableShares,
|
||||
r.unpaidAccounts,
|
||||
r.unpaidShares,
|
||||
r.unpaidAmount,
|
||||
this.distinctiveRange(r),
|
||||
r.accountCount,
|
||||
r.memberCount,
|
||||
`${r.minimumClientShares ?? '—'} / ${r.nominalClientShares ?? '—'} / ${r.maximumClientShares ?? '—'}`
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
print(): void {
|
||||
shareReportPrint();
|
||||
}
|
||||
|
||||
private readError(err: any, fallback: string): string {
|
||||
return (
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
fallback
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -278,6 +278,23 @@ $srep-muted: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* A share class's own authorised-vs-issued fraction, read at a glance rather than only in the two
|
||||
numbers beside it. */
|
||||
.product-utilisation-bar {
|
||||
width: 100%;
|
||||
min-width: 90px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: $srep-border-soft;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-utilisation-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #2563eb, #059669);
|
||||
}
|
||||
|
||||
/* Something the reader has to know before trusting the number above it — an exclusion, a gap, a
|
||||
figure that will not reconcile until someone acts. Loud enough to be read, not an error. */
|
||||
.srep-warn {
|
||||
|
||||
Reference in New Issue
Block a user