feat(share): add Share Collection cashier flow and fix Share Product accounting
- Add Share Collection screen under Cashier menu for cash pre-collection before share account activation; posts Dr Cash Vault, Cr Share Sundry, Cr Income GL entries with duplicate-collection guard - Restore all 4 GL mappings (Assets, Liabilities, Income, Equity) in Share Product accounting step with pill-style savings-product UI - Remove Multiples section from Share Product currency step; default Decimal Places to 2 - GET /cashier/sharecollection/:id endpoint for collection status check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5f7de01ba9
commit
c8e9ab8f84
@@ -0,0 +1,294 @@
|
||||
<!--
|
||||
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="pg">
|
||||
<!-- ── HEADER ── -->
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">CASHIER · SHARE COLLECTION</div>
|
||||
<h1 class="pg-title">Share Collection</h1>
|
||||
<p class="pg-sub">
|
||||
Collect cash from member for share purchase — amounts are held in Sundry GL until the account is activated.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── SEARCH BAR ── -->
|
||||
@if (pageState !== 'success') {
|
||||
<div class="sc-search-bar">
|
||||
<div class="sc-search-bar__input-wrap">
|
||||
<mat-icon class="sc-search-bar__icon">search</mat-icon>
|
||||
<input
|
||||
class="sc-search-bar__input"
|
||||
type="text"
|
||||
placeholder="Enter Share Account ID or Account No."
|
||||
[(ngModel)]="accountInput"
|
||||
(keyup.enter)="searchAccount()"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--primary"
|
||||
[disabled]="pageState === 'searching' || !accountInput.trim()"
|
||||
(click)="searchAccount()"
|
||||
>
|
||||
<mat-icon>manage_search</mat-icon>
|
||||
{{ pageState === 'searching' ? 'Searching…' : 'Search' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── ERROR ── -->
|
||||
@if (errorMsg) {
|
||||
<div class="sc-alert sc-alert--error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ errorMsg }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── SEARCHING SPINNER ── -->
|
||||
@if (pageState === 'searching') {
|
||||
<div class="pg-empty">
|
||||
<mat-icon class="sc-spin">hourglass_empty</mat-icon>
|
||||
<p>Looking up account…</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── ACCOUNT LOADED ── -->
|
||||
@if (pageState === 'loaded') {
|
||||
<!-- Member Card -->
|
||||
<div class="sc-member-card">
|
||||
<div class="sc-member-card__avatar">
|
||||
<mat-icon>person</mat-icon>
|
||||
</div>
|
||||
<div class="sc-member-card__info">
|
||||
<div class="sc-member-card__name">{{ account.clientName }}</div>
|
||||
<div class="sc-member-card__meta">
|
||||
<span class="sc-badge sc-badge--account">{{ account.accountNo }}</span>
|
||||
<span class="sc-badge sc-badge--product">{{ account.productName }}</span>
|
||||
<span
|
||||
class="sc-badge"
|
||||
[class.sc-badge--pending]="account.status?.code?.includes('pending')"
|
||||
[class.sc-badge--active]="account.status?.code?.includes('active')"
|
||||
>{{ account.status?.value }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editable Shares + Price -->
|
||||
<div class="sc-member-card__shares">
|
||||
<div class="sc-shares-inputs">
|
||||
<div class="sc-shares-field">
|
||||
<label class="sc-shares-label">No. of Shares</label>
|
||||
<input
|
||||
class="sc-shares-input"
|
||||
type="number"
|
||||
min="0"
|
||||
[(ngModel)]="requestedSharesInput"
|
||||
(input)="recalc()"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="sc-shares-sep">×</div>
|
||||
<div class="sc-shares-field">
|
||||
<label class="sc-shares-label">Price / Share (₹)</label>
|
||||
<input
|
||||
class="sc-shares-input"
|
||||
type="number"
|
||||
min="0"
|
||||
[(ngModel)]="pricePerShare"
|
||||
(input)="recalc()"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
<div class="sc-shares-sep">=</div>
|
||||
<div class="sc-shares-result">
|
||||
<div class="sc-shares-label">Share Amount</div>
|
||||
<div class="sc-shares-total">{{ currencySymbol }}{{ shareAmount | number: '1.2-2' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount Breakdown + GL Preview -->
|
||||
<div class="sc-split">
|
||||
<!-- LEFT: Amount Breakdown -->
|
||||
<div class="pg-card sc-breakdown">
|
||||
<div class="sc-breakdown__header">
|
||||
<mat-icon>receipt_long</mat-icon>
|
||||
<span>Amount Breakdown</span>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Share Amount
|
||||
<span class="sc-dim">
|
||||
({{ requestedSharesInput }} shares × {{ currencySymbol }}{{ pricePerShare | number: '1.2-2' }})
|
||||
</span>
|
||||
</td>
|
||||
<td class="num">{{ currencySymbol }}{{ shareAmount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
@for (charge of charges; track charge.id) {
|
||||
<tr>
|
||||
<td>{{ charge.name }}</td>
|
||||
<td class="num sc-charge-amount">
|
||||
<input class="sc-charge-input" type="number" min="0" [(ngModel)]="charge.amount" (input)="recalc()" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (charges.length === 0) {
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="sc-dim">No charges applicable</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="sc-total-row">
|
||||
<td><strong>Total Payable</strong></td>
|
||||
<td class="num sc-total-amount">
|
||||
<strong>{{ currencySymbol }}{{ totalAmount | number: '1.2-2' }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: GL Preview -->
|
||||
<div class="pg-card sc-gl-preview">
|
||||
<div class="sc-breakdown__header">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
<span>GL Entries Preview</span>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account</th>
|
||||
<th>Dr / Cr</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="gl-row--dr">
|
||||
<td class="gl-acname">Cash Vault GL</td>
|
||||
<td><span class="sc-dr-badge">Dr</span></td>
|
||||
<td class="num gl-amt--dr">{{ currencySymbol }}{{ totalAmount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
<tr class="gl-row--cr">
|
||||
<td class="gl-acname">Share Sundry GL</td>
|
||||
<td><span class="sc-cr-badge">Cr</span></td>
|
||||
<td class="num gl-amt--cr">{{ currencySymbol }}{{ shareAmount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
@if (totalChargeAmount > 0) {
|
||||
<tr class="gl-row--cr">
|
||||
<td class="gl-acname">Charge Receivable GL</td>
|
||||
<td><span class="sc-cr-badge">Cr</span></td>
|
||||
<td class="num gl-amt--cr">{{ currencySymbol }}{{ totalChargeAmount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sc-gl-note">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
Funds move to Share GL & Income GL when account is <strong>activated</strong>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Details -->
|
||||
<div class="pg-toolbar pg-toolbar--mat">
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>Transaction Date *</mat-label>
|
||||
<input matInput type="date" [(ngModel)]="transactionDate" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>Payment Mode</mat-label>
|
||||
<mat-select [(ngModel)]="paymentTypeId">
|
||||
<mat-option [value]="null">— Cash (Default) —</mat-option>
|
||||
@for (pt of paymentTypes; track pt.id) {
|
||||
<mat-option [value]="pt.id">{{ pt.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="pg-note-field">
|
||||
<mat-label>Note (Optional)</mat-label>
|
||||
<input matInput type="text" [(ngModel)]="note" placeholder="e.g. Share purchase for 100 shares" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Footer Submit Bar -->
|
||||
<div class="pg-total-bar sc-footer">
|
||||
<div class="sc-footer__summary">
|
||||
<span class="sc-footer__label">Total to Collect</span>
|
||||
<span class="sc-footer__amount">{{ currencySymbol }}{{ totalAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="sc-footer__actions">
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="reset()"><mat-icon>close</mat-icon> Cancel</button>
|
||||
<button type="button" class="pg-btn pg-btn--primary" [disabled]="!isValid || saving" (click)="submit()">
|
||||
<mat-icon>payments</mat-icon>
|
||||
{{ saving ? 'Posting…' : 'Collect & Post' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── SUCCESS ── -->
|
||||
@if (pageState === 'success' && collectionResult) {
|
||||
<div class="sc-success">
|
||||
<div class="sc-success__icon-wrap">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
</div>
|
||||
<h2 class="sc-success__title">Collection Posted Successfully</h2>
|
||||
<p class="sc-success__sub">
|
||||
Cash of
|
||||
<strong>{{ collectionResult.currencyCode }} {{ collectionResult.totalAmount | number: '1.2-2' }}</strong>
|
||||
collected from <strong>{{ collectionResult.clientName }}</strong> on
|
||||
<strong>{{ collectionResult.collectionDate }}</strong> by <strong>{{ collectionResult.collectedBy }}</strong
|
||||
>.
|
||||
</p>
|
||||
|
||||
<!-- Transaction reference -->
|
||||
<div class="sc-success__ref">
|
||||
<span class="sc-success__ref-label">Transaction ID</span>
|
||||
<span class="sc-success__ref-val">{{ collectionResult.transactionId }}</span>
|
||||
</div>
|
||||
|
||||
<!-- GL entries from backend -->
|
||||
<div class="sc-success__gl">
|
||||
@for (entry of collectionResult.glEntries; track entry.account) {
|
||||
<div
|
||||
class="sc-success__gl-row"
|
||||
[class.sc-success__gl-row--dr]="entry.type === 'Dr'"
|
||||
[class.sc-success__gl-row--cr]="entry.type === 'Cr'"
|
||||
>
|
||||
<span>{{ entry.type }}. {{ entry.account }}</span>
|
||||
<span>{{ entry.currency }} {{ entry.amount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p class="sc-success__note">
|
||||
Funds are held in Sundry GL. They will move to Share Equity GL when the account is activated.
|
||||
</p>
|
||||
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="reset()">
|
||||
<mat-icon>add</mat-icon> New Collection
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
Reference in New Issue
Block a user