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
@@ -20,6 +20,7 @@ import { ReconciliationComponent } from './reconciliation/reconciliation.compone
|
|||||||
import { CashHistoryComponent } from './cash-history/cash-history.component';
|
import { CashHistoryComponent } from './cash-history/cash-history.component';
|
||||||
import { ClosingCashReportComponent } from './closing-cash-report/closing-cash-report.component';
|
import { ClosingCashReportComponent } from './closing-cash-report/closing-cash-report.component';
|
||||||
import { ExcessTransactionReportComponent } from './excess-transaction-report/excess-transaction-report.component';
|
import { ExcessTransactionReportComponent } from './excess-transaction-report/excess-transaction-report.component';
|
||||||
|
import { ShareCollectionComponent } from './share-collection/share-collection.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -76,6 +77,11 @@ const routes: Routes = [
|
|||||||
path: 'excess-report',
|
path: 'excess-report',
|
||||||
component: ExcessTransactionReportComponent,
|
component: ExcessTransactionReportComponent,
|
||||||
data: { title: 'Excess Transaction Report', breadcrumb: 'Excess Transaction Report' }
|
data: { title: 'Excess Transaction Report', breadcrumb: 'Excess Transaction Report' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'share-collection',
|
||||||
|
component: ShareCollectionComponent,
|
||||||
|
data: { title: 'Share Collection', breadcrumb: 'Share Collection' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { ReconciliationComponent } from './reconciliation/reconciliation.compone
|
|||||||
import { CashHistoryComponent } from './cash-history/cash-history.component';
|
import { CashHistoryComponent } from './cash-history/cash-history.component';
|
||||||
import { ClosingCashReportComponent } from './closing-cash-report/closing-cash-report.component';
|
import { ClosingCashReportComponent } from './closing-cash-report/closing-cash-report.component';
|
||||||
import { ExcessTransactionReportComponent } from './excess-transaction-report/excess-transaction-report.component';
|
import { ExcessTransactionReportComponent } from './excess-transaction-report/excess-transaction-report.component';
|
||||||
|
import { ShareCollectionComponent } from './share-collection/share-collection.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -34,7 +35,8 @@ import { ExcessTransactionReportComponent } from './excess-transaction-report/ex
|
|||||||
ReconciliationComponent,
|
ReconciliationComponent,
|
||||||
CashHistoryComponent,
|
CashHistoryComponent,
|
||||||
ClosingCashReportComponent,
|
ClosingCashReportComponent,
|
||||||
ExcessTransactionReportComponent
|
ExcessTransactionReportComponent,
|
||||||
|
ShareCollectionComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CashierModule {}
|
export class CashierModule {}
|
||||||
|
|||||||
@@ -156,4 +156,22 @@ export class CashierService {
|
|||||||
if (asOfDate) params = params.set('asOfDate', asOfDate);
|
if (asOfDate) params = params.set('asOfDate', asOfDate);
|
||||||
return this.http.get('/cashreconciliation/expected/me', { params });
|
return this.http.get('/cashreconciliation/expected/me', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Payment Types ──
|
||||||
|
getPaymentTypes(): Observable<any[]> {
|
||||||
|
return this.http.get<any[]>('/paymenttypes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Share Collection ──
|
||||||
|
getShareAccountForCollection(accountId: string): Observable<any> {
|
||||||
|
return this.http.get(`/accounts/share/${accountId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
collectSharePayment(data: any): Observable<any> {
|
||||||
|
return this.http.post('/cashier/sharecollection', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
getShareCollectionStatus(shareAccountId: number): Observable<any> {
|
||||||
|
return this.http.get(`/cashier/sharecollection/${shareAccountId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Search Bar ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
|
||||||
|
&__input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #0d1b2a;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Alert ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Spinner ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-spin {
|
||||||
|
animation: sc-spin 1.2s linear infinite;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 8px;
|
||||||
|
font-size: 32px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sc-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Member Card ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-member-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eff6ff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__shares {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Badges ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
&--account {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--product {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Editable Share Inputs ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-shares-inputs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-shares-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-shares-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-shares-input {
|
||||||
|
width: 110px;
|
||||||
|
height: 38px;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #1353a0;
|
||||||
|
text-align: right;
|
||||||
|
outline: none;
|
||||||
|
background: #f8fafc;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1353a0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-shares-sep {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-shares-result {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-shares-total {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f4f3c;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Editable Charge Amount ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-charge-amount {
|
||||||
|
padding: 4px 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-charge-input {
|
||||||
|
width: 100px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #1353a0;
|
||||||
|
text-align: right;
|
||||||
|
outline: none;
|
||||||
|
background: #f8fafc;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1353a0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Split Layout ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Breakdown Card ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-breakdown {
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #0f4f3c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-gl-preview {
|
||||||
|
.sc-breakdown__header {
|
||||||
|
mat-icon {
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-gl-note {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Table helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-dim {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-total-row td {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-top: 2px solid #e2e8f0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-total-amount {
|
||||||
|
color: #0f4f3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dr / Cr badges ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-dr-badge,
|
||||||
|
.sc-cr-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-dr-badge {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-cr-badge {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Footer ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-footer {
|
||||||
|
justify-content: space-between !important;
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f4f3c;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Success ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.sc-success {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
gap: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
&__icon-wrap {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f0fdf4;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sub {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__ref {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__ref-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: #94a3b8;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__ref-val {
|
||||||
|
font-family: 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1353a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__note {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__gl {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__gl-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px dotted #e2e8f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dr {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--cr {
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* 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 { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { CashierService } from '../cashier.service';
|
||||||
|
|
||||||
|
type PageState = 'idle' | 'searching' | 'loaded' | 'submitting' | 'success';
|
||||||
|
|
||||||
|
interface ShareCharge {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
currencySymbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-share-collection',
|
||||||
|
templateUrl: './share-collection.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../cashier-shared.scss',
|
||||||
|
'./share-collection.component.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInputModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ShareCollectionComponent implements OnInit {
|
||||||
|
private service = inject(CashierService);
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
|
pageState: PageState = 'idle';
|
||||||
|
accountInput = '';
|
||||||
|
transactionDate = '';
|
||||||
|
paymentTypeId: number | null = null;
|
||||||
|
paymentTypes: any[] = [];
|
||||||
|
note = '';
|
||||||
|
errorMsg = '';
|
||||||
|
saving = false;
|
||||||
|
|
||||||
|
account: any = null;
|
||||||
|
charges: ShareCharge[] = [];
|
||||||
|
collectionResult: any = null; // filled after successful POST
|
||||||
|
|
||||||
|
// Editable fields — cashier can override if API returns 0
|
||||||
|
requestedSharesInput = 0;
|
||||||
|
pricePerShare = 0;
|
||||||
|
|
||||||
|
get shareAmount(): number {
|
||||||
|
return this.requestedSharesInput * this.pricePerShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalChargeAmount(): number {
|
||||||
|
return this.charges.reduce((s, c) => s + c.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalAmount(): number {
|
||||||
|
return this.shareAmount + this.totalChargeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currencySymbol(): string {
|
||||||
|
return this.account?.currency?.displaySymbol ?? '₹';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isValid(): boolean {
|
||||||
|
return !!this.account && this.totalAmount > 0 && !!this.transactionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const d = new Date();
|
||||||
|
this.transactionDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
|
this.service
|
||||||
|
.getPaymentTypes()
|
||||||
|
.pipe(catchError(() => of([])))
|
||||||
|
.subscribe((types: any[]) => (this.paymentTypes = types || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
searchAccount(): void {
|
||||||
|
const query = this.accountInput.trim();
|
||||||
|
if (!query) return;
|
||||||
|
this.pageState = 'searching';
|
||||||
|
this.account = null;
|
||||||
|
this.charges = [];
|
||||||
|
this.errorMsg = '';
|
||||||
|
|
||||||
|
this.service
|
||||||
|
.getShareAccountForCollection(query)
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.errorMsg =
|
||||||
|
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||||
|
err?.error?.developerMessage ||
|
||||||
|
'Account not found. Please check the account ID and try again.';
|
||||||
|
this.pageState = 'idle';
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((acc: any) => {
|
||||||
|
if (!acc) return;
|
||||||
|
this.account = acc;
|
||||||
|
|
||||||
|
// Auto-detect shares from summary or first pending purchasedShares entry
|
||||||
|
const pendingTx = (acc.purchasedShares || []).find(
|
||||||
|
(t: any) =>
|
||||||
|
t.status?.code === 'purchasedSharesStatusType.applied' && t.type?.code === 'purchasedSharesType.purchased'
|
||||||
|
);
|
||||||
|
this.requestedSharesInput = Number(
|
||||||
|
acc.summary?.totalPendingForApprovalShares || pendingTx?.numberOfShares || acc.requestedShares || 0
|
||||||
|
);
|
||||||
|
this.pricePerShare = Number(pendingTx?.purchasedPrice || acc.currentMarketPrice || acc.unitPrice || 0);
|
||||||
|
|
||||||
|
// Show ALL charges on the account — no type filtering
|
||||||
|
this.charges = (acc.charges || []).map((c: any) => ({
|
||||||
|
id: c.chargeId ?? c.id,
|
||||||
|
name: c.name,
|
||||||
|
amount: Number(c.amount ?? c.amountOutstanding ?? 0),
|
||||||
|
currencySymbol: c.currency?.displaySymbol ?? '₹'
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.pageState = 'loaded';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recalc(): void {
|
||||||
|
// triggered by input changes — nothing extra needed, getters auto-recalculate
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
if (!this.isValid || this.saving) return;
|
||||||
|
this.saving = true;
|
||||||
|
const payload = {
|
||||||
|
shareAccountId: this.account.id,
|
||||||
|
accountNo: this.account.accountNo,
|
||||||
|
clientName: this.account.clientName,
|
||||||
|
transactionDate: this.transactionDate,
|
||||||
|
requestedShares: this.requestedSharesInput,
|
||||||
|
pricePerShare: this.pricePerShare,
|
||||||
|
shareAmount: this.shareAmount,
|
||||||
|
chargeAmount: this.totalChargeAmount,
|
||||||
|
totalAmount: this.totalAmount,
|
||||||
|
paymentTypeId: this.paymentTypeId || null,
|
||||||
|
note: this.note || null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.service.collectSharePayment(payload).subscribe({
|
||||||
|
next: (result: any) => {
|
||||||
|
this.collectionResult = result;
|
||||||
|
this.snackBar.open('Share collection posted successfully.', 'Close', { duration: 3000 });
|
||||||
|
this.pageState = 'success';
|
||||||
|
this.saving = false;
|
||||||
|
},
|
||||||
|
error: (err: any) => {
|
||||||
|
const msg =
|
||||||
|
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||||
|
err?.error?.developerMessage ||
|
||||||
|
err?.error?.defaultUserMessage ||
|
||||||
|
'Collection failed. Please try again.';
|
||||||
|
this.snackBar.open(msg, 'Close', { duration: 8000 });
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.pageState = 'idle';
|
||||||
|
this.accountInput = '';
|
||||||
|
this.account = null;
|
||||||
|
this.charges = [];
|
||||||
|
this.errorMsg = '';
|
||||||
|
this.note = '';
|
||||||
|
this.paymentTypeId = null;
|
||||||
|
this.requestedSharesInput = 0;
|
||||||
|
this.pricePerShare = 0;
|
||||||
|
this.collectionResult = null;
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -725,6 +725,9 @@
|
|||||||
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/cash-history']">
|
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/cash-history']">
|
||||||
<fa-icon icon="history" class="menu-item-icon"></fa-icon> Cash History
|
<fa-icon icon="history" class="menu-item-icon"></fa-icon> Cash History
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item tabindex="0" [routerLink]="['/cashier/share-collection']">
|
||||||
|
<fa-icon icon="hand-holding-usd" class="menu-item-icon"></fa-icon> Share Collection
|
||||||
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
<mat-menu #depositsMenu="matMenu" class="deposits-mega-panel" [overlapTrigger]="false">
|
<mat-menu #depositsMenu="matMenu" class="deposits-mega-panel" [overlapTrigger]="false">
|
||||||
|
|||||||
+101
-86
@@ -7,96 +7,111 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<form [formGroup]="shareProductAccountingForm">
|
<form [formGroup]="shareProductAccountingForm">
|
||||||
<!-- Accounting Rule -->
|
|
||||||
<div class="shpa-section">
|
<div class="shpa-section">
|
||||||
<div class="shpa-section__header">
|
<div class="shpa-section__header">
|
||||||
<i class="fas fa-balance-scale"></i>
|
<span class="shpa-section__icon">
|
||||||
<h3>{{ 'labels.heading.Accounting Rule' | translate }}</h3>
|
<i class="fas fa-book"></i>
|
||||||
|
</span>
|
||||||
|
<h3>Accounting</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="shpa-grid">
|
|
||||||
<div class="shpa-field--full">
|
<div class="shpa-section__body">
|
||||||
<mat-radio-group class="shpa-radio-group" formControlName="accountingRule">
|
<!-- Accounting Rule -->
|
||||||
@for (accountingRule of accountingRuleData; track accountingRule; let i = $index) {
|
<div class="shpa-group">
|
||||||
<mat-radio-button [value]="i + 1">
|
<div class="shpa-group__header">
|
||||||
{{ 'labels.accounting.' + accountingRule | translate }}
|
<h4>Accounting Rule</h4>
|
||||||
</mat-radio-button>
|
</div>
|
||||||
}
|
<div class="shpa-radio-group">
|
||||||
</mat-radio-group>
|
<mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Sections -->
|
||||||
|
@for (section of accountingSectionConfigs; track section.key) {
|
||||||
|
<div class="shpa-group">
|
||||||
|
<div class="shpa-group__header">
|
||||||
|
<h4>{{ section.title }}</h4>
|
||||||
|
<button type="button" class="shpa-add-btn" (click)="addOtherGlMapping(section.otherMappingsControlName)">
|
||||||
|
+ Add Other
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shpa-list">
|
||||||
|
@for (row of section.rows; track row.key) {
|
||||||
|
<div class="shpa-row">
|
||||||
|
<div class="shpa-row__meta">
|
||||||
|
<span
|
||||||
|
class="shpa-pill"
|
||||||
|
[class.shpa-pill--green]="section.pillClass === 'green'"
|
||||||
|
[class.shpa-pill--amber]="section.pillClass === 'amber'"
|
||||||
|
[class.shpa-pill--blue]="section.pillClass === 'blue'"
|
||||||
|
>
|
||||||
|
{{ section.pillLabel }}
|
||||||
|
</span>
|
||||||
|
<span class="shpa-row__label">{{ row.label }}</span>
|
||||||
|
</div>
|
||||||
|
<mifosx-gl-account-selector
|
||||||
|
class="shpa-row__input"
|
||||||
|
[inputFormControl]="shareProductAccountingForm.get(row.controlName)"
|
||||||
|
[glAccountList]="row.options"
|
||||||
|
[required]="row.required"
|
||||||
|
[inputLabel]="row.inputLabel"
|
||||||
|
></mifosx-gl-account-selector>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shpa-list" [formArrayName]="section.otherMappingsControlName">
|
||||||
|
@for (
|
||||||
|
otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
|
||||||
|
track $index;
|
||||||
|
let i = $index
|
||||||
|
) {
|
||||||
|
<div class="shpa-other-row-wrap">
|
||||||
|
<div class="shpa-row shpa-row--other" [formGroupName]="i">
|
||||||
|
<div class="shpa-row__meta shpa-row__meta--other">
|
||||||
|
<span
|
||||||
|
class="shpa-pill"
|
||||||
|
[class.shpa-pill--green]="section.pillClass === 'green'"
|
||||||
|
[class.shpa-pill--amber]="section.pillClass === 'amber'"
|
||||||
|
[class.shpa-pill--blue]="section.pillClass === 'blue'"
|
||||||
|
>
|
||||||
|
{{ section.pillLabel }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
class="shpa-row__name shpa-row__name--other-account"
|
||||||
|
formControlName="accountName"
|
||||||
|
placeholder="Enter account name..."
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="shpa-row__actions shpa-row__actions--other">
|
||||||
|
<mat-form-field class="shpa-row__other-field" appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>GL Code</mat-label>
|
||||||
|
<mat-select
|
||||||
|
[formControl]="getOtherGlMappingControl(section.otherMappingsControlName, i, 'glAccountId')"
|
||||||
|
>
|
||||||
|
@for (glAccount of section.otherAccountOptions; track glAccount.id) {
|
||||||
|
<mat-option [value]="glAccount.id">
|
||||||
|
{{ '(' + glAccount.glCode + ') ' + glAccount.name }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shpa-remove-btn shpa-remove-btn--side"
|
||||||
|
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (shareProductAccountingForm.value.accountingRule === 2) {
|
|
||||||
<!-- Assets -->
|
|
||||||
<div class="shpa-section">
|
|
||||||
<div class="shpa-section__header">
|
|
||||||
<i class="fas fa-building"></i>
|
|
||||||
<h3>{{ 'labels.heading.Assets' | translate }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="shpa-grid">
|
|
||||||
<mifosx-gl-account-selector
|
|
||||||
class="shpa-field"
|
|
||||||
[inputFormControl]="shareProductAccountingForm.controls.shareReferenceId"
|
|
||||||
[glAccountList]="assetAccountData"
|
|
||||||
[required]="true"
|
|
||||||
[inputLabel]="'Share reference'"
|
|
||||||
>
|
|
||||||
</mifosx-gl-account-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Liabilities -->
|
|
||||||
<div class="shpa-section">
|
|
||||||
<div class="shpa-section__header">
|
|
||||||
<i class="fas fa-hand-holding-usd"></i>
|
|
||||||
<h3>{{ 'labels.heading.Liabilities' | translate }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="shpa-grid">
|
|
||||||
<mifosx-gl-account-selector
|
|
||||||
class="shpa-field"
|
|
||||||
[inputFormControl]="shareProductAccountingForm.controls.shareSuspenseId"
|
|
||||||
[glAccountList]="liabilityAccountData"
|
|
||||||
[required]="true"
|
|
||||||
[inputLabel]="'Share suspense control'"
|
|
||||||
>
|
|
||||||
</mifosx-gl-account-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Share Equity -->
|
|
||||||
<div class="shpa-section">
|
|
||||||
<div class="shpa-section__header">
|
|
||||||
<i class="fas fa-chart-pie"></i>
|
|
||||||
<h3>{{ 'labels.heading.Share Equity' | translate }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="shpa-grid">
|
|
||||||
<mifosx-gl-account-selector
|
|
||||||
class="shpa-field"
|
|
||||||
[inputFormControl]="shareProductAccountingForm.controls.shareEquityId"
|
|
||||||
[glAccountList]="equityAccountData"
|
|
||||||
[required]="true"
|
|
||||||
[inputLabel]="'Equity'"
|
|
||||||
>
|
|
||||||
</mifosx-gl-account-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Income -->
|
|
||||||
<div class="shpa-section">
|
|
||||||
<div class="shpa-section__header">
|
|
||||||
<i class="fas fa-money-bill-wave"></i>
|
|
||||||
<h3>{{ 'labels.heading.Income' | translate }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="shpa-grid">
|
|
||||||
<mifosx-gl-account-selector
|
|
||||||
class="shpa-field"
|
|
||||||
[inputFormControl]="shareProductAccountingForm.controls.incomeFromFeeAccountId"
|
|
||||||
[glAccountList]="incomeAccountData"
|
|
||||||
[required]="true"
|
|
||||||
[inputLabel]="'Income from fees'"
|
|
||||||
>
|
|
||||||
</mifosx-gl-account-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+266
-35
@@ -6,54 +6,73 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$border: #e2e8f0;
|
||||||
|
$gray-50: #f8f9fa;
|
||||||
|
$gray-200: #e8eaed;
|
||||||
|
$gray-500: #6b7280;
|
||||||
|
$gray-700: #334155;
|
||||||
|
$gray-900: #202124;
|
||||||
|
$primary: #1353a0;
|
||||||
|
|
||||||
.shpa-section {
|
.shpa-section {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid $border;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-section__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: $gray-50;
|
||||||
|
border-bottom: 1px solid $gray-200;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-section__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-section__body {
|
||||||
|
padding: 16px 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-group {
|
||||||
& + & {
|
& + & {
|
||||||
margin-top: 16px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: space-between;
|
||||||
padding: 12px 24px;
|
gap: 16px;
|
||||||
background: #f8f9fa;
|
margin: 0 0 12px;
|
||||||
border-bottom: 1px solid #e8eaed;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #0ea5e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #202124;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.shpa-grid {
|
h4 {
|
||||||
display: grid;
|
margin: 0 0 12px;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
font-size: 12px;
|
||||||
gap: 4px 16px;
|
font-weight: 700;
|
||||||
padding: 16px 24px;
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
@media (width <= 600px) {
|
color: #94a3b8;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shpa-field {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&--full {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,3 +81,215 @@
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shpa-add-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-list + .shpa-list {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row:has(mifosx-gl-account-selector) {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row--other {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #f8fafc;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-other-row-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__meta--other {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__input {
|
||||||
|
flex: 0 0 22rem;
|
||||||
|
max-width: 22rem;
|
||||||
|
min-width: 18rem;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__input :is(mifosx-gl-account-selector) {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__actions--other {
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__name {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 24rem;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__name--other-account {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__other-field {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__other-field :is(mat-form-field) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-row__other-field :is(.mat-mdc-form-field-required-marker) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-remove-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-remove-btn--side {
|
||||||
|
place-self: center end;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
justify-self: start;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shpa-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--amber {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+176
-38
@@ -8,15 +8,44 @@
|
|||||||
|
|
||||||
import { Component, OnInit, Input, inject } from '@angular/core';
|
import { Component, OnInit, Input, inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
FormsModule,
|
||||||
UntypedFormGroup,
|
UntypedFormGroup,
|
||||||
UntypedFormBuilder,
|
UntypedFormBuilder,
|
||||||
Validators,
|
UntypedFormArray,
|
||||||
UntypedFormControl,
|
UntypedFormControl,
|
||||||
|
Validators,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||||
import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component';
|
import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
import { GLAccount } from 'app/shared/models/general.model';
|
||||||
|
|
||||||
|
type ShareAccountingSectionKey = 'assets' | 'liabilities' | 'income' | 'equity';
|
||||||
|
|
||||||
|
type ShareAccountingRowConfig = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
inputLabel: string;
|
||||||
|
controlName: string;
|
||||||
|
options: GLAccount[];
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShareAccountingSectionConfig = {
|
||||||
|
key: ShareAccountingSectionKey;
|
||||||
|
title: string;
|
||||||
|
pillLabel: string;
|
||||||
|
pillClass: 'green' | 'amber' | 'blue';
|
||||||
|
rows: ShareAccountingRowConfig[];
|
||||||
|
otherMappingsControlName: string;
|
||||||
|
otherAccountOptions: GLAccount[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type OtherGlMappingValue = {
|
||||||
|
accountName: string;
|
||||||
|
glAccountId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-share-product-accounting-step',
|
selector: 'mifosx-share-product-accounting-step',
|
||||||
@@ -24,6 +53,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
styleUrls: ['./share-product-accounting-step.component.scss'],
|
styleUrls: ['./share-product-accounting-step.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
MatRadioGroup,
|
MatRadioGroup,
|
||||||
MatRadioButton,
|
MatRadioButton,
|
||||||
GlAccountSelectorComponent
|
GlAccountSelectorComponent
|
||||||
@@ -37,65 +68,172 @@ export class ShareProductAccountingStepComponent implements OnInit {
|
|||||||
@Input() shareProductFormValid: boolean;
|
@Input() shareProductFormValid: boolean;
|
||||||
|
|
||||||
shareProductAccountingForm: UntypedFormGroup;
|
shareProductAccountingForm: UntypedFormGroup;
|
||||||
|
accountingSectionConfigs: ShareAccountingSectionConfig[] = [];
|
||||||
|
|
||||||
assetAccountData: any;
|
assetAccountData: GLAccount[] = [];
|
||||||
incomeAccountData: any;
|
liabilityAccountData: GLAccount[] = [];
|
||||||
equityAccountData: any;
|
incomeAccountData: GLAccount[] = [];
|
||||||
liabilityAccountData: any;
|
equityAccountData: GLAccount[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.createShareProductAccountingForm();
|
this.createShareProductAccountingForm();
|
||||||
this.setConditionalControls();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.assetAccountData = this.shareProductsTemplate.accountingMappingOptions.assetAccountOptions || [];
|
const opts = this.shareProductsTemplate.accountingMappingOptions || {};
|
||||||
this.incomeAccountData = this.shareProductsTemplate.accountingMappingOptions.incomeAccountOptions || [];
|
this.assetAccountData = opts.assetAccountOptions || [];
|
||||||
this.equityAccountData = this.shareProductsTemplate.accountingMappingOptions.equityAccountOptions || [];
|
this.liabilityAccountData = opts.liabilityAccountOptions || [];
|
||||||
this.liabilityAccountData = this.shareProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
|
this.incomeAccountData = opts.incomeAccountOptions || [];
|
||||||
|
this.equityAccountData = opts.equityAccountOptions || [];
|
||||||
|
|
||||||
if (this.shareProductsTemplate.accountingRule) {
|
this.buildAccountingSectionConfigs();
|
||||||
|
|
||||||
|
if (this.shareProductsTemplate.accountingMappings) {
|
||||||
|
const m = this.shareProductsTemplate.accountingMappings;
|
||||||
this.shareProductAccountingForm.patchValue({
|
this.shareProductAccountingForm.patchValue({
|
||||||
accountingRule: this.shareProductsTemplate.accountingRule.id
|
shareReferenceId: m.shareReferenceId?.id || '',
|
||||||
|
shareSuspenseId: m.shareSuspenseId?.id || '',
|
||||||
|
incomeFromFeeAccountId: m.incomeFromFeeAccountId?.id || '',
|
||||||
|
shareEquityId: m.shareEquityId?.id || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.shareProductsTemplate.accountingRule.id === 2) {
|
|
||||||
this.shareProductAccountingForm.patchValue({
|
|
||||||
shareReferenceId: this.shareProductsTemplate.accountingMappings.shareReferenceId.id,
|
|
||||||
shareSuspenseId: this.shareProductsTemplate.accountingMappings.shareSuspenseId.id,
|
|
||||||
shareEquityId: this.shareProductsTemplate.accountingMappings.shareEquityId.id,
|
|
||||||
incomeFromFeeAccountId: this.shareProductsTemplate.accountingMappings.incomeFromFeeAccountId.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createShareProductAccountingForm() {
|
createShareProductAccountingForm() {
|
||||||
this.shareProductAccountingForm = this.formBuilder.group({
|
this.shareProductAccountingForm = this.formBuilder.group({
|
||||||
accountingRule: [1]
|
accountingRule: [2],
|
||||||
|
shareReferenceId: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
shareSuspenseId: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
incomeFromFeeAccountId: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
shareEquityId: [
|
||||||
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
assetsOtherGlMappings: this.formBuilder.array([]),
|
||||||
|
liabilitiesOtherGlMappings: this.formBuilder.array([]),
|
||||||
|
incomeOtherGlMappings: this.formBuilder.array([]),
|
||||||
|
equityOtherGlMappings: this.formBuilder.array([])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setConditionalControls() {
|
buildAccountingSectionConfigs() {
|
||||||
this.shareProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => {
|
this.accountingSectionConfigs = [
|
||||||
if (accountingRule === 2) {
|
{
|
||||||
this.shareProductAccountingForm.addControl('shareReferenceId', new UntypedFormControl('', Validators.required));
|
key: 'assets',
|
||||||
this.shareProductAccountingForm.addControl('shareSuspenseId', new UntypedFormControl('', Validators.required));
|
title: 'Assets',
|
||||||
this.shareProductAccountingForm.addControl('shareEquityId', new UntypedFormControl('', Validators.required));
|
pillLabel: 'Asset',
|
||||||
this.shareProductAccountingForm.addControl(
|
pillClass: 'green',
|
||||||
'incomeFromFeeAccountId',
|
rows: [
|
||||||
new UntypedFormControl('', Validators.required)
|
{
|
||||||
);
|
key: 'shareReferenceId',
|
||||||
} else {
|
label: 'Share Reference (Cash Vault)',
|
||||||
this.shareProductAccountingForm.removeControl('shareReferenceId');
|
inputLabel: 'Share reference',
|
||||||
this.shareProductAccountingForm.removeControl('shareSuspenseId');
|
controlName: 'shareReferenceId',
|
||||||
this.shareProductAccountingForm.removeControl('shareEquityId');
|
options: this.assetAccountData,
|
||||||
this.shareProductAccountingForm.removeControl('incomeFromFeeAccountId');
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
otherMappingsControlName: 'assetsOtherGlMappings',
|
||||||
|
otherAccountOptions: this.assetAccountData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'liabilities',
|
||||||
|
title: 'Liabilities',
|
||||||
|
pillLabel: 'Liability',
|
||||||
|
pillClass: 'green',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
key: 'shareSuspenseId',
|
||||||
|
label: 'Share Sundry (Suspense)',
|
||||||
|
inputLabel: 'Share sundry / suspense',
|
||||||
|
controlName: 'shareSuspenseId',
|
||||||
|
options: this.liabilityAccountData,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
otherMappingsControlName: 'liabilitiesOtherGlMappings',
|
||||||
|
otherAccountOptions: this.liabilityAccountData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'income',
|
||||||
|
title: 'Income',
|
||||||
|
pillLabel: 'Income',
|
||||||
|
pillClass: 'blue',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
key: 'incomeFromFeeAccountId',
|
||||||
|
label: 'Income From Fees',
|
||||||
|
inputLabel: 'Income from fees',
|
||||||
|
controlName: 'incomeFromFeeAccountId',
|
||||||
|
options: this.incomeAccountData,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
otherMappingsControlName: 'incomeOtherGlMappings',
|
||||||
|
otherAccountOptions: this.incomeAccountData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'equity',
|
||||||
|
title: 'Share Equity',
|
||||||
|
pillLabel: 'Equity',
|
||||||
|
pillClass: 'blue',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
key: 'shareEquityId',
|
||||||
|
label: 'Share Equity',
|
||||||
|
inputLabel: 'Share equity',
|
||||||
|
controlName: 'shareEquityId',
|
||||||
|
options: this.equityAccountData,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
otherMappingsControlName: 'equityOtherGlMappings',
|
||||||
|
otherAccountOptions: this.equityAccountData
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getOtherGlMappingsArray(controlName: string): UntypedFormArray {
|
||||||
|
return this.shareProductAccountingForm.get(controlName) as UntypedFormArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
addOtherGlMapping(controlName: string) {
|
||||||
|
this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOtherGlMapping(controlName: string, index: number) {
|
||||||
|
this.getOtherGlMappingsArray(controlName).removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl {
|
||||||
|
return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createOtherGlMappingGroup(val: Partial<OtherGlMappingValue> = {}) {
|
||||||
|
return this.formBuilder.group({
|
||||||
|
accountName: [
|
||||||
|
val.accountName || '',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
glAccountId: [
|
||||||
|
val.glAccountId || '',
|
||||||
|
Validators.required
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get shareProductAccounting() {
|
get shareProductAccounting() {
|
||||||
return this.shareProductAccountingForm.value;
|
const { assetsOtherGlMappings, liabilitiesOtherGlMappings, incomeOtherGlMappings, equityOtherGlMappings, ...rest } =
|
||||||
|
this.shareProductAccountingForm.value;
|
||||||
|
return rest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-31
@@ -49,35 +49,4 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shpcr-section">
|
|
||||||
<div class="shpcr-section__header">
|
|
||||||
<i class="fas fa-calculator"></i>
|
|
||||||
<h3>{{ 'labels.inputs.Multiples' | translate }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="shpcr-grid">
|
|
||||||
<div class="shpcr-field--full shpcr-checkbox-row">
|
|
||||||
<mat-checkbox labelPosition="before" formControlName="setMultiples">
|
|
||||||
{{ 'labels.inputs.Set the share installment in multiples of' | translate }}
|
|
||||||
</mat-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (shareProductCurrencyForm.value.setMultiples) {
|
|
||||||
<mat-form-field appearance="outline" class="shpcr-field">
|
|
||||||
<mat-label>{{ 'labels.inputs.Currency in multiples of' | translate }}</mat-label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
matInput
|
|
||||||
matTooltip="{{ 'tooltips.Amount to be rounded off' | translate }}"
|
|
||||||
formControlName="inMultiplesOf"
|
|
||||||
/>
|
|
||||||
<mat-error>
|
|
||||||
{{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }}
|
|
||||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+6
-48
@@ -6,11 +6,9 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core';
|
import { Component, OnInit, Input, inject } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
@@ -21,13 +19,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
imports: [
|
imports: [
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
FaIconComponent,
|
FaIconComponent
|
||||||
MatCheckbox
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ShareProductCurrencyStepComponent implements OnInit {
|
export class ShareProductCurrencyStepComponent implements OnInit {
|
||||||
private formBuilder = inject(UntypedFormBuilder);
|
private formBuilder = inject(UntypedFormBuilder);
|
||||||
private destroyRef = inject(DestroyRef);
|
|
||||||
|
|
||||||
@Input() shareProductsTemplate: any;
|
@Input() shareProductsTemplate: any;
|
||||||
|
|
||||||
@@ -44,12 +40,8 @@ export class ShareProductCurrencyStepComponent implements OnInit {
|
|||||||
|
|
||||||
this.shareProductCurrencyForm.patchValue({
|
this.shareProductCurrencyForm.patchValue({
|
||||||
currencyCode: this.shareProductsTemplate.currency?.code || this.currencyData[0].code,
|
currencyCode: this.shareProductsTemplate.currency?.code || this.currencyData[0].code,
|
||||||
digitsAfterDecimal: this.shareProductsTemplate.digitsAfterDecimal ?? '',
|
digitsAfterDecimal: this.shareProductsTemplate.digitsAfterDecimal ?? 2
|
||||||
setMultiples: !!this.shareProductsTemplate.inMultiplesOf,
|
|
||||||
inMultiplesOf: this.shareProductsTemplate.inMultiplesOf ?? ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setupConditionalValidation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createShareProductCurrencyForm() {
|
createShareProductCurrencyForm() {
|
||||||
@@ -59,54 +51,20 @@ export class ShareProductCurrencyStepComponent implements OnInit {
|
|||||||
Validators.required
|
Validators.required
|
||||||
],
|
],
|
||||||
digitsAfterDecimal: [
|
digitsAfterDecimal: [
|
||||||
'',
|
2,
|
||||||
[
|
[
|
||||||
Validators.required,
|
Validators.required,
|
||||||
Validators.min(0)
|
Validators.min(0)
|
||||||
]
|
]
|
||||||
],
|
]
|
||||||
setMultiples: [false],
|
|
||||||
inMultiplesOf: ['']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupConditionalValidation() {
|
|
||||||
const inMultiplesOfControl = this.shareProductCurrencyForm.get('inMultiplesOf');
|
|
||||||
const setMultiplesControl = this.shareProductCurrencyForm.get('setMultiples');
|
|
||||||
|
|
||||||
const applyInMultiplesValidators = (checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
inMultiplesOfControl?.setValidators([
|
|
||||||
Validators.required,
|
|
||||||
Validators.min(1)
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
inMultiplesOfControl?.clearValidators();
|
|
||||||
inMultiplesOfControl?.setValue('');
|
|
||||||
}
|
|
||||||
inMultiplesOfControl?.updateValueAndValidity();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply validators based on initial value
|
|
||||||
applyInMultiplesValidators(setMultiplesControl?.value);
|
|
||||||
|
|
||||||
// Listen for changes
|
|
||||||
setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => {
|
|
||||||
applyInMultiplesValidators(checked);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get shareProductCurrency() {
|
get shareProductCurrency() {
|
||||||
const formValue = this.shareProductCurrencyForm.value;
|
const formValue = this.shareProductCurrencyForm.value;
|
||||||
const result: any = {
|
return {
|
||||||
currencyCode: formValue.currencyCode,
|
currencyCode: formValue.currencyCode,
|
||||||
digitsAfterDecimal: formValue.digitsAfterDecimal
|
digitsAfterDecimal: formValue.digitsAfterDecimal
|
||||||
};
|
};
|
||||||
|
|
||||||
if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) {
|
|
||||||
result.inMultiplesOf = formValue.inMultiplesOf;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user