Merge dev into demo-backup
Brings in all of dev's latest content not present on demo-backup (Share Withdrawal/Closure feature, TDS reports, KYC exception tracking, client-edit-email-dialog, form121 tab, search-select) while preserving demo-backup's own unique changes (wrapping Approval Center tab nav, Loan Origination/Investment mega-menus). 36 files that a prior "ours wins" conflict resolution had silently dropped were restored explicitly from dev's tip.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
<!--
|
||||
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="passbook-container">
|
||||
<!-- Filter Bar -->
|
||||
<div class="passbook-filter-bar no-print">
|
||||
<form [formGroup]="dateRangeForm" class="passbook-filter-form">
|
||||
<mat-form-field appearance="outline" class="passbook-date-field" (click)="fromDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.From Date' | translate }}</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="fromDatePicker" formControlName="fromDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="fromDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="passbook-date-field" (click)="toDatePicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.To Date' | translate }}</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="toDatePicker" formControlName="toDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="toDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!dateRangeForm.valid"
|
||||
(click)="filterTransactions()"
|
||||
>
|
||||
<mat-icon>filter_list</mat-icon>
|
||||
{{ 'labels.buttons.Filter' | translate }}
|
||||
</button>
|
||||
|
||||
@if (isFiltered) {
|
||||
<button mat-stroked-button type="button" (click)="clearFilter()">
|
||||
<mat-icon>clear</mat-icon>
|
||||
{{ 'labels.buttons.Clear' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<span class="passbook-filter-spacer"></span>
|
||||
|
||||
<button mat-raised-button color="accent" type="button" (click)="printPassbook()">
|
||||
<mat-icon>print</mat-icon>
|
||||
{{ 'labels.buttons.Print' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Share Passbook -->
|
||||
<div class="statement-wrapper" #passbookContent>
|
||||
<!-- Bank Header -->
|
||||
<div class="bank-header">
|
||||
<div class="bank-logo-area">
|
||||
<div class="bank-logo-circle">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
</div>
|
||||
<div class="bank-name-block">
|
||||
<div class="bank-title">LincPay Co-operative Bank Ltd.</div>
|
||||
<div class="bank-subtitle">Share Account Passbook</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statement-label">
|
||||
<div class="stmt-label-text">SHARE HOLDING STATEMENT</div>
|
||||
<div class="stmt-period">As on: {{ maxDate | dateFormat }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Details -->
|
||||
<div class="account-details-band">
|
||||
<div class="account-details-grid">
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Member Name</div>
|
||||
<div class="detail-value bold">{{ clientName | uppercase }}</div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Share Account No.</div>
|
||||
<div class="detail-value mono bold">{{ accountNo }}</div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Product</div>
|
||||
<div class="detail-value">{{ productName }}</div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Account Opening Date</div>
|
||||
<div class="detail-value">{{ activatedOnDate | dateFormat }}</div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Status</div>
|
||||
<div class="detail-value status-badge" [class.active]="status === 'Active'">{{ status }}</div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Linked Savings A/C No.</div>
|
||||
<div class="detail-value mono">{{ savingsAccountNumber || '—' }}</div>
|
||||
</div>
|
||||
@if (resolutionNumber) {
|
||||
<div class="detail-block">
|
||||
<div class="detail-label">Resolution No.</div>
|
||||
<div class="detail-value">{{ resolutionNumber }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Balance Summary Strip -->
|
||||
<div class="balance-summary-strip">
|
||||
<div class="balance-box">
|
||||
<div class="balance-box-label">Approved Shares</div>
|
||||
<div class="balance-box-value">{{ totalApprovedShares }}</div>
|
||||
</div>
|
||||
<div class="balance-divider"></div>
|
||||
<div class="balance-box">
|
||||
<div class="balance-box-label">Share Value</div>
|
||||
<div class="balance-box-value">
|
||||
{{ currentMarketPrice | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="balance-divider"></div>
|
||||
<div class="balance-box">
|
||||
<div class="balance-box-label">Current Value</div>
|
||||
<div class="balance-box-value">{{ currentValue | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}</div>
|
||||
</div>
|
||||
<div class="balance-divider"></div>
|
||||
<div class="balance-box">
|
||||
<div class="balance-box-label">Total Transactions</div>
|
||||
<div class="balance-box-value">{{ filteredTransactions.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions Table -->
|
||||
@if (filteredTransactions.length > 0) {
|
||||
<table class="statement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-sr">#</th>
|
||||
<th class="col-date">Date</th>
|
||||
<th class="col-desc">Description</th>
|
||||
<th class="col-ref">Ref No.</th>
|
||||
<th class="col-amount text-right">Shares Purchased</th>
|
||||
<th class="col-amount text-right">Shares Redeemed</th>
|
||||
<th class="col-balance text-right">Share Balance</th>
|
||||
<th class="col-amount text-right">Amount (₹)</th>
|
||||
<th class="col-desc">Resolution No. / Meeting</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (txn of filteredTransactions; track txn.id; let i = $index) {
|
||||
<tr [class.debit-row]="isRedeem(txn)" [class.credit-row]="!isRedeem(txn)">
|
||||
<td class="col-sr text-center">{{ i + 1 }}</td>
|
||||
<td class="col-date">{{ txn.purchasedDate | dateFormat }}</td>
|
||||
<td class="col-desc">
|
||||
<span class="txn-desc">{{ txn.type.value }} ({{ txn.status.value }})</span>
|
||||
</td>
|
||||
<td class="col-ref text-muted mono">{{ txn.id }}</td>
|
||||
<td class="col-amount text-right credit-amount">
|
||||
@if (!isRedeem(txn)) {
|
||||
{{ txn.numberOfShares }}
|
||||
}
|
||||
</td>
|
||||
<td class="col-amount text-right debit-amount">
|
||||
@if (isRedeem(txn)) {
|
||||
{{ txn.numberOfShares }}
|
||||
}
|
||||
</td>
|
||||
<td class="col-balance text-right balance-amount">{{ txn.runningBalance }}</td>
|
||||
<td class="col-amount text-right">
|
||||
{{ txn.amount | currency: currency?.code : 'symbol-narrow' : '1.2-2' }}
|
||||
</td>
|
||||
<td class="col-desc">
|
||||
@if (isRedeem(txn)) {
|
||||
{{ txn.resolutionNumber || '—' }}
|
||||
@if (txn.meetingDate) {
|
||||
({{ txn.meetingDate | dateFormat }})
|
||||
}
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td colspan="6" class="text-right total-label">Closing Share Balance</td>
|
||||
<td class="text-right closing-balance">{{ totalApprovedShares }}</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
} @else {
|
||||
<div class="no-data-block">
|
||||
<mat-icon>inbox</mat-icon>
|
||||
<p>No transactions found for the selected date range.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Statement Footer -->
|
||||
<div class="statement-footer">
|
||||
<div class="footer-note">
|
||||
This is a computer-generated statement and does not require a signature. For queries, contact your branch or
|
||||
call customer care.
|
||||
</div>
|
||||
<div class="footer-meta">Generated on: {{ maxDate | dateFormat }} | Account No: {{ accountNo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,462 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// ─── Variables ──────────────────────────────────────────────────────────────
|
||||
$bank-primary: #1a3a6b; // deep navy
|
||||
$bank-accent: #c8a951; // gold trim
|
||||
$debit-color: #c0392b;
|
||||
$credit-color: #1a7a3f;
|
||||
$balance-color: #1a3a6b;
|
||||
$border-color: #d0d7e3;
|
||||
$row-alt: #f4f7fb;
|
||||
|
||||
// ─── Container ──────────────────────────────────────────────────────────────
|
||||
.passbook-container {
|
||||
padding: 16px;
|
||||
background: #eef1f7;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
// ─── Filter Bar ─────────────────────────────────────────────────────────────
|
||||
.passbook-filter-bar {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.passbook-filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.passbook-date-field {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.passbook-filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// ─── Statement Wrapper ───────────────────────────────────────────────────────
|
||||
.statement-wrapper {
|
||||
background: #fff;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgb(0 0 0 / 8%);
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
}
|
||||
|
||||
// ─── Bank Header ─────────────────────────────────────────────────────────────
|
||||
.bank-header {
|
||||
background: $bank-primary;
|
||||
padding: 18px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 4px solid $bank-accent;
|
||||
}
|
||||
|
||||
.bank-logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bank-logo-circle {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: $bank-accent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
mat-icon {
|
||||
color: $bank-primary;
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.bank-name-block {
|
||||
.bank-title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.bank-subtitle {
|
||||
color: $bank-accent;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.statement-label {
|
||||
text-align: right;
|
||||
|
||||
.stmt-label-text {
|
||||
color: $bank-accent;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stmt-period {
|
||||
color: rgb(255 255 255 / 75%);
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Account Details Band ────────────────────────────────────────────────────
|
||||
.account-details-band {
|
||||
background: #f0f3f9;
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: 14px 24px;
|
||||
}
|
||||
|
||||
.account-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
.detail-label {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #7a8aaa;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 13px;
|
||||
color: #222;
|
||||
|
||||
&.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
&.status-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: #eee;
|
||||
color: #555;
|
||||
|
||||
&.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Balance Summary Strip ───────────────────────────────────────────────────
|
||||
.balance-summary-strip {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: $bank-primary;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.balance-box {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
text-align: center;
|
||||
|
||||
.balance-box-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: rgb(255 255 255 / 65%);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.balance-box-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: $bank-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-divider {
|
||||
width: 1px;
|
||||
background: rgb(255 255 255 / 15%);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
// ─── Transactions Table ───────────────────────────────────────────────────────
|
||||
.statement-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
|
||||
thead tr {
|
||||
background: #e8edf5;
|
||||
border-top: 1px solid $border-color;
|
||||
border-bottom: 2px solid $bank-primary;
|
||||
}
|
||||
|
||||
thead th {
|
||||
padding: 9px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: $bank-primary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid #e8ecf3;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: $row-alt;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #dce6f5;
|
||||
}
|
||||
}
|
||||
|
||||
tbody td {
|
||||
padding: 7px 10px;
|
||||
vertical-align: middle;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
tfoot .total-row {
|
||||
background: #e8edf5;
|
||||
border-top: 2px solid $bank-primary;
|
||||
|
||||
td {
|
||||
padding: 9px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Column widths ────────────────────────────────────────────────────────────
|
||||
.col-sr {
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.col-date {
|
||||
width: 100px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-desc {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.col-ref {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.col-amount {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.col-balance {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
// ─── Amount colors ────────────────────────────────────────────────────────────
|
||||
.debit-amount {
|
||||
color: $debit-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.credit-amount {
|
||||
color: $credit-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
color: $balance-color;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: $bank-primary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.closing-balance {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: $bank-primary;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
// ─── Badges & misc ────────────────────────────────────────────────────────────
|
||||
.txn-desc {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
// ─── No Data ──────────────────────────────────────────────────────────────────
|
||||
.no-data-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: #aaa;
|
||||
|
||||
mat-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Statement Footer ─────────────────────────────────────────────────────────
|
||||
.statement-footer {
|
||||
background: #f0f3f9;
|
||||
border-top: 1px solid $border-color;
|
||||
padding: 12px 24px;
|
||||
text-align: center;
|
||||
|
||||
.footer-note {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Responsive ───────────────────────────────────────────────────────────────
|
||||
@media (width <= 768px) {
|
||||
.account-details-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.bank-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.statement-label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.balance-summary-strip {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.balance-divider {
|
||||
width: auto;
|
||||
height: 1px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.passbook-filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.passbook-date-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.passbook-filter-spacer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 480px) {
|
||||
.account-details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Print ────────────────────────────────────────────────────────────────────
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.passbook-container {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.statement-wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.bank-header {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.balance-summary-strip {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, ElementRef, ViewChild, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UntypedFormBuilder, Validators } from '@angular/forms';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Share Account Passbook — a printable, chronological (oldest-first) ledger of every
|
||||
* Purchase/Redeem entry, mirroring PassbookTabComponent (Savings) but for share holdings:
|
||||
* shares purchased/redeemed instead of amount deposited/withdrawn, plus the Board Resolution
|
||||
* Number / Date of Meeting on redeem rows.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-shares-passbook-tab',
|
||||
templateUrl: './passbook-tab.component.html',
|
||||
styleUrls: ['./passbook-tab.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatIcon,
|
||||
DateFormatPipe,
|
||||
CurrencyPipe
|
||||
]
|
||||
})
|
||||
export class SharesPassbookTabComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
|
||||
@ViewChild('passbookContent') passbookContent: ElementRef;
|
||||
|
||||
/** Account data */
|
||||
accountNo = '';
|
||||
clientName = '';
|
||||
productName = '';
|
||||
currency: any = null;
|
||||
status = '';
|
||||
activatedOnDate: any = null;
|
||||
resolutionNumber = '';
|
||||
savingsAccountNumber = '';
|
||||
totalApprovedShares = 0;
|
||||
currentMarketPrice = 0;
|
||||
|
||||
/** Transactions */
|
||||
allTransactions: any[] = [];
|
||||
filteredTransactions: any[] = [];
|
||||
|
||||
/** Date range form */
|
||||
minDate = new Date(2000, 0, 1);
|
||||
maxDate = new Date();
|
||||
dateRangeForm: any;
|
||||
isFiltered = false;
|
||||
|
||||
get currentValue(): number {
|
||||
return this.totalApprovedShares * this.currentMarketPrice;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.parent.data.subscribe((data: { sharesAccountData: any }) => {
|
||||
const account = data.sharesAccountData;
|
||||
this.accountNo = account.accountNo || '';
|
||||
this.clientName = account.clientName || '';
|
||||
this.productName = account.productName || '';
|
||||
this.currency = account.currency || null;
|
||||
this.status = account.status?.value || '';
|
||||
this.activatedOnDate = account.timeline?.activatedDate || null;
|
||||
this.resolutionNumber = account.resolutionNumber || '';
|
||||
this.savingsAccountNumber = account.savingsAccountNumber || '';
|
||||
this.totalApprovedShares = account.summary?.totalApprovedShares || 0;
|
||||
this.currentMarketPrice = account.currentMarketPrice || 0;
|
||||
|
||||
const transactions = (account.purchasedShares || []).filter(
|
||||
(transaction: any) => transaction.type.value !== 'Charge Payment'
|
||||
);
|
||||
this.allTransactions = this.recalculateRunningBalances(this.sortAscending(transactions));
|
||||
this.filteredTransactions = [...this.allTransactions];
|
||||
});
|
||||
|
||||
this.dateRangeForm = this.formBuilder.group({
|
||||
fromDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
toDate: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
isRedeem(transaction: any): boolean {
|
||||
return transaction.type?.value === 'Redeem';
|
||||
}
|
||||
|
||||
filterTransactions(): void {
|
||||
const fromDate = this.dateRangeForm.value.fromDate;
|
||||
const toDate = this.dateRangeForm.value.toDate;
|
||||
|
||||
if (!fromDate || !toDate) return;
|
||||
|
||||
const from = new Date(fromDate).getTime();
|
||||
const to = new Date(toDate).getTime();
|
||||
|
||||
this.filteredTransactions = this.allTransactions.filter((txn: any) => {
|
||||
const txnDate = this.getTransactionDateAsTime(txn.purchasedDate);
|
||||
return txnDate >= from && txnDate <= to;
|
||||
});
|
||||
this.isFiltered = true;
|
||||
}
|
||||
|
||||
clearFilter(): void {
|
||||
this.dateRangeForm.reset();
|
||||
this.filteredTransactions = [...this.allTransactions];
|
||||
this.isFiltered = false;
|
||||
}
|
||||
|
||||
private getTransactionDateAsTime(date: any): number {
|
||||
if (Array.isArray(date) && date.length >= 3) {
|
||||
return new Date(date[0], date[1] - 1, date[2]).getTime();
|
||||
}
|
||||
return new Date(date).getTime();
|
||||
}
|
||||
|
||||
printPassbook(): void {
|
||||
const content = this.passbookContent?.nativeElement?.innerHTML;
|
||||
if (!content) return;
|
||||
|
||||
const printWindow = window.open('', 'SharePassbook', 'height=700,width=900');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Share Account Passbook - ${this.accountNo}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; padding: 20px; color: #333; }
|
||||
.passbook-header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #1a3a6b; padding-bottom: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||
th { background: #1a3a6b; color: #fff; padding: 6px 8px; text-align: left; font-weight: 600; }
|
||||
td { padding: 5px 8px; border-bottom: 1px solid #e0e0e0; }
|
||||
tr:nth-child(even) { background: #fafafa; }
|
||||
.text-right { text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
printWindow.print();
|
||||
printWindow.close();
|
||||
}
|
||||
|
||||
private recalculateRunningBalances(transactions: any[]): any[] {
|
||||
let balance = 0;
|
||||
return transactions.map((txn) => {
|
||||
const shares = Number(txn.numberOfShares) || 0;
|
||||
balance += this.isRedeem(txn) ? -shares : shares;
|
||||
return { ...txn, runningBalance: balance };
|
||||
});
|
||||
}
|
||||
|
||||
private sortAscending(transactions: any[]): any[] {
|
||||
return [...transactions].sort((a, b) => {
|
||||
const dateA = this.getTransactionDateAsTime(a.purchasedDate);
|
||||
const dateB = this.getTransactionDateAsTime(b.purchasedDate);
|
||||
if (dateA !== dateB) return dateA - dateB;
|
||||
return (a.id as number) - (b.id as number);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,13 @@
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content class="content">
|
||||
@if (sharesAccountData.status?.value === 'Active' && (sharesAccountData.summary?.totalApprovedShares ?? 0) === 0) {
|
||||
<div class="closure-eligible-banner">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
<span>All shares have been withdrawn from this account — it is now eligible for closure.</span>
|
||||
<a class="closure-eligible-banner__link" [routerLink]="['/clerk/share-closure-request']"> Request Closure </a>
|
||||
</div>
|
||||
}
|
||||
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
|
||||
<a
|
||||
mat-tab-link
|
||||
|
||||
@@ -14,3 +14,33 @@
|
||||
min-width: 60%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.closure-eligible-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
margin: 0 0 12px;
|
||||
background: #eaf2ff;
|
||||
border: 1px solid rgb(52 87 213 / 25%);
|
||||
border-left: 3px solid #3457d5;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #1b3a8a;
|
||||
|
||||
fa-icon {
|
||||
color: #3457d5;
|
||||
}
|
||||
|
||||
&__link {
|
||||
margin-left: auto;
|
||||
font-weight: 700;
|
||||
color: #3457d5;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,11 +102,11 @@ export class SharesButtonsConfiguration {
|
||||
{
|
||||
name: 'Share Certificate',
|
||||
taskPermissionName: 'READ_SHAREACCOUNTPURCHASE'
|
||||
},
|
||||
{
|
||||
name: 'Close',
|
||||
taskPermissionName: 'CLOSE_SHAREACCOUNT'
|
||||
}
|
||||
// 'Close' is intentionally not offered here — closing a share account must go through
|
||||
// the Clerk Dashboard maker-checker flow (Clerk Dashboard > My Share Closures), never a
|
||||
// direct one-step action from this view. The native close command also force-redeems any
|
||||
// remaining shares, bypassing the governed Share Withdrawal approval flow entirely.
|
||||
];
|
||||
break;
|
||||
case 'Submitted and pending approval':
|
||||
|
||||
Reference in New Issue
Block a user