283 lines
11 KiB
HTML
283 lines
11 KiB
HTML
<!--
|
|
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="exchange" class="cert-head-ic"></fa-icon>
|
|
<div>
|
|
<h2 class="cert-head-t">Share Transfer</h2>
|
|
<div class="cert-head-s">
|
|
Moving shares from one member to another, under board resolution — and the register of every transfer made.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="cert-head-r">
|
|
<button class="cert-cta-btn" (click)="exportCsv()" [disabled]="loading || !filteredRows.length">
|
|
<fa-icon icon="file-alt"></fa-icon> Export CSV
|
|
</button>
|
|
<button class="cert-cta-btn" (click)="print()" [disabled]="loading || !filteredRows.length">
|
|
<fa-icon icon="print"></fa-icon> Print
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (totals) {
|
|
<div class="cert-kpis">
|
|
<div class="cert-kpi cert-kpi--blue">
|
|
<fa-icon icon="exchange" class="cert-kpi-ic"></fa-icon>
|
|
<div>
|
|
<div class="cert-kpi-n">{{ totals.transferCount | number }}</div>
|
|
<div class="cert-kpi-l">Transfers</div>
|
|
</div>
|
|
</div>
|
|
<div class="cert-kpi" [ngClass]="totals.pendingCount ? 'cert-kpi--amber' : 'cert-kpi--blue'">
|
|
<fa-icon icon="sync" class="cert-kpi-ic"></fa-icon>
|
|
<div>
|
|
<div class="cert-kpi-n">{{ totals.pendingCount | number }}</div>
|
|
<div class="cert-kpi-l">Awaiting Approval</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.sharesTransferred | number }}</div>
|
|
<div class="cert-kpi-l">Shares Transferred</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.valueTransferred | number: '1.2-2' }}</div>
|
|
<div class="cert-kpi-l">Value Transferred</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Tabs -->
|
|
<div class="cert-filters srep-no-print">
|
|
<div class="cert-filter">
|
|
<label> </label>
|
|
<button class="cert-cta-btn" [class.srep-tab--active]="tab === 'register'" (click)="tab = 'register'">
|
|
Transfer register
|
|
</button>
|
|
</div>
|
|
<div class="cert-filter">
|
|
<label> </label>
|
|
<button class="cert-cta-btn" [class.srep-tab--active]="tab === 'new'" (click)="tab = 'new'">New transfer</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (tab === 'new') {
|
|
<!-- ── Raise a transfer ── -->
|
|
<div class="srep-stmt-head" style="display: block">
|
|
<div class="cert-filters" style="margin: 0">
|
|
<div class="cert-filter" style="flex: 2 1 300px; position: relative">
|
|
<label>Transfer FROM (member giving up shares)</label>
|
|
<div class="cert-input-ic">
|
|
<fa-icon icon="search"></fa-icon>
|
|
<input
|
|
type="text"
|
|
[ngModel]="fromSearch"
|
|
(ngModelChange)="onFromSearch($event)"
|
|
placeholder="Search member or share account number"
|
|
/>
|
|
</div>
|
|
@if (fromOptions.length && !fromAccount) {
|
|
<div class="srep-picker">
|
|
@for (opt of fromOptions; track opt.id) {
|
|
<button type="button" class="srep-picker-item" (click)="pickFrom(opt)">
|
|
<span class="srep-picker-name">{{ opt.clientName }}</span>
|
|
<span class="srep-picker-meta srep-mono">{{ opt.accountNo }}</span>
|
|
<span class="srep-picker-meta">
|
|
{{ opt.productName }} · holds {{ opt.totalApprovedShares | number }}
|
|
</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="cert-filter" style="flex: 2 1 300px; position: relative">
|
|
<label>Transfer TO (member receiving shares)</label>
|
|
<div class="cert-input-ic">
|
|
<fa-icon icon="search"></fa-icon>
|
|
<input
|
|
type="text"
|
|
[ngModel]="toSearch"
|
|
(ngModelChange)="onToSearch($event)"
|
|
placeholder="Search member or share account number"
|
|
/>
|
|
</div>
|
|
@if (toOptions.length && !toAccount) {
|
|
<div class="srep-picker">
|
|
@for (opt of toOptions; track opt.id) {
|
|
<button type="button" class="srep-picker-item" (click)="pickTo(opt)">
|
|
<span class="srep-picker-name">{{ opt.clientName }}</span>
|
|
<span class="srep-picker-meta srep-mono">{{ opt.accountNo }}</span>
|
|
<span class="srep-picker-meta">
|
|
{{ opt.productName }} · holds {{ opt.totalApprovedShares | number }}
|
|
</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="cert-filter">
|
|
<label>Shares to transfer</label>
|
|
<input type="number" min="1" [(ngModel)]="shares" placeholder="e.g. 20" />
|
|
</div>
|
|
|
|
<div class="cert-filter">
|
|
<label>Transfer date</label>
|
|
<input type="date" [(ngModel)]="transferDate" />
|
|
</div>
|
|
|
|
<div class="cert-filter">
|
|
<label>Board resolution no.</label>
|
|
<input type="text" [(ngModel)]="resolutionNumber" placeholder="e.g. BR-2026-41" />
|
|
</div>
|
|
|
|
<div class="cert-filter">
|
|
<label>Board meeting date</label>
|
|
<input type="date" [(ngModel)]="meetingDate" />
|
|
</div>
|
|
|
|
<div class="cert-filter" style="flex: 2 1 300px">
|
|
<label>Reason (optional)</label>
|
|
<input type="text" [(ngModel)]="reason" placeholder="e.g. transfer to legal heir" />
|
|
</div>
|
|
|
|
<div class="cert-filter">
|
|
<label> </label>
|
|
<button class="cert-cta-btn" (click)="submit()" [disabled]="!canSubmit">
|
|
{{ submitting ? 'Submitting…' : 'Submit for approval' }}
|
|
</button>
|
|
</div>
|
|
<div class="cert-filter">
|
|
<label> </label>
|
|
<button class="cert-cta-btn" (click)="resetForm()">Clear</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (fromAccount && toAccount && shares) {
|
|
<div class="srep-scope-note">
|
|
{{ shares | number }} shares of {{ fromAccount.productName }} move from
|
|
<b>{{ fromAccount.clientName }}</b> ({{ fromAccount.accountNo }}) to <b>{{ toAccount.clientName }}</b> ({{
|
|
toAccount.accountNo
|
|
}}). Share capital does not change — only the register does. Nothing moves until a checker approves it.
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (submitError) {
|
|
<div class="srep-state srep-state--error"><fa-icon icon="exclamation-circle"></fa-icon> {{ submitError }}</div>
|
|
}
|
|
@if (submitSuccess) {
|
|
<div class="srep-state"><fa-icon icon="check-circle"></fa-icon> {{ submitSuccess }}</div>
|
|
}
|
|
}
|
|
|
|
<!-- ── Register ── -->
|
|
<div class="cert-filters" [class.srep-no-print]="tab === 'new'">
|
|
<div class="cert-filter">
|
|
<label>Member / Reference / Resolution</label>
|
|
<div class="cert-input-ic">
|
|
<fa-icon icon="search"></fa-icon>
|
|
<input type="text" [(ngModel)]="search" placeholder="e.g. member name or TRF-260917-0001" />
|
|
</div>
|
|
</div>
|
|
<div class="cert-filter">
|
|
<label>Status</label>
|
|
<select [(ngModel)]="statusFilter" (change)="loadRegister()">
|
|
<option value="ALL">All</option>
|
|
<option value="PENDING">Pending approval</option>
|
|
<option value="APPROVED">Approved</option>
|
|
<option value="REJECTED">Rejected</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
@if (loading) {
|
|
<div class="srep-state">Loading the transfer register…</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 transfer has been recorded yet.</div>
|
|
} @else if (!filteredRows.length) {
|
|
<div class="srep-state">No transfer matches the filters applied.</div>
|
|
} @else {
|
|
<div class="srep-table-wrap">
|
|
<table class="srep-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Reference</th>
|
|
<th>Date</th>
|
|
<th>From</th>
|
|
<th>To</th>
|
|
<th>Product</th>
|
|
<th class="srep-num">Shares</th>
|
|
<th class="srep-num">Value</th>
|
|
<th>Board Resolution</th>
|
|
<th>Status</th>
|
|
<th class="srep-no-print">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (row of filteredRows; track row.transferId) {
|
|
<tr>
|
|
<td class="srep-mono">{{ row.transferReference }}</td>
|
|
<td>{{ row.transferDate }}</td>
|
|
<td>
|
|
{{ row.fromClientName }}
|
|
<div class="srep-scope-note srep-mono" style="margin: 0">{{ row.fromAccountNo }}</div>
|
|
</td>
|
|
<td>
|
|
{{ row.toClientName }}
|
|
<div class="srep-scope-note srep-mono" style="margin: 0">{{ row.toAccountNo }}</div>
|
|
</td>
|
|
<td>{{ row.productName }}</td>
|
|
<td class="srep-num">{{ row.shares | number }}</td>
|
|
<td class="srep-num">₹{{ row.transferAmount | number: '1.2-2' }}</td>
|
|
<td class="srep-mono">
|
|
{{ row.resolutionNumber || '—' }}
|
|
@if (row.meetingDate) {
|
|
<div class="srep-scope-note" style="margin: 0">{{ row.meetingDate }}</div>
|
|
}
|
|
</td>
|
|
<td>
|
|
<span class="srep-pill" [ngClass]="statusClass(row.status)">{{ statusLabel(row.status) }}</span>
|
|
@if (row.status === 'REJECTED' && row.rejectionReason) {
|
|
<div class="srep-scope-note" style="margin: 0">{{ row.rejectionReason }}</div>
|
|
}
|
|
@if (row.status !== 'PENDING' && row.checkerName) {
|
|
<div class="srep-scope-note" style="margin: 0">by {{ row.checkerName }}</div>
|
|
}
|
|
</td>
|
|
<td class="srep-no-print">
|
|
@if (row.status === 'PENDING') {
|
|
<button class="cert-cta-btn" (click)="approve(row)" [disabled]="actioningId === row.transferId">
|
|
{{ actioningId === row.transferId ? '…' : 'Approve' }}
|
|
</button>
|
|
<button class="cert-cta-btn" (click)="reject(row)" [disabled]="actioningId === row.transferId">
|
|
Reject
|
|
</button>
|
|
} @else {
|
|
<span class="srep-scope-note">{{ row.actionedOn || '—' }}</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|