Revert demo to pre-2026-08-06 state (rollback today's push)

Reverts all commits pushed to demo today (a5124ca3, 858c277a, a1a50ab8,
f72abb09, fcf66657) back to f8c532f7 as a single forward commit, without
rewriting history. All reverted content (Share Withdrawal/Closure feature,
TDS reports, KYC exception tracking, client-edit-email-dialog, form121 tab,
search-select) remains intact on dev and was verified identical before
this revert.
This commit is contained in:
2026-08-06 13:39:40 +05:30
parent fcf6665772
commit 14c5f5070d
87 changed files with 1102 additions and 14920 deletions
@@ -20,11 +20,11 @@
</div>
<div class="table-filters">
<mat-form-field class="table-search" appearance="outline" subscriptSizing="dynamic">
<mat-form-field class="table-search" appearance="outline">
<input matInput placeholder="Search by client name or account number" [formControl]="searchControl" />
</mat-form-field>
<mat-form-field class="table-filter" appearance="outline" subscriptSizing="dynamic">
<mat-form-field class="table-filter" appearance="outline">
<mat-select [(value)]="branchFilter" (selectionChange)="applyFilters()" placeholder="Branch">
<mat-option value="">All Branches</mat-option>
<mat-option *ngFor="let office of offices" [value]="office.id">
@@ -33,7 +33,7 @@
</mat-select>
</mat-form-field>
<mat-form-field class="table-filter" appearance="outline" subscriptSizing="dynamic">
<mat-form-field class="table-filter" appearance="outline">
<mat-select [(value)]="productFilter" (selectionChange)="applyFilters()" placeholder="Share Product">
<mat-option value="">All Products</mat-option>
<mat-option *ngFor="let product of products" [value]="product.id">
@@ -42,7 +42,7 @@
</mat-select>
</mat-form-field>
<mat-form-field class="table-filter" appearance="outline" subscriptSizing="dynamic">
<mat-form-field class="table-filter" appearance="outline">
<mat-select [(value)]="statusFilter" (selectionChange)="applyFilters()" placeholder="Status">
<mat-option value="">All Status</mat-option>
<mat-option value="active">Active</mat-option>
@@ -53,7 +53,7 @@
</mat-select>
</mat-form-field>
<mat-form-field class="table-filter" appearance="outline" subscriptSizing="dynamic" (click)="picker.open()">
<mat-form-field class="table-filter" appearance="outline" (click)="picker.open()">
<mat-date-range-input [formGroup]="dateRangeForm" [rangePicker]="picker">
<input matStartDate formControlName="start" placeholder="Start date" (dateChange)="applyFilters()" />
<input matEndDate formControlName="end" placeholder="End date" (dateChange)="applyFilters()" />
@@ -5,43 +5,37 @@
* 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/.
*/
:host {
display: block;
width: 100%;
}
.container {
margin: 1.5rem auto;
max-width: 1400px;
padding: 20px 24px;
border-radius: 16px;
background: #ffffff;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
box-sizing: border-box;
margin: 1.5rem;
padding: 0;
border-radius: var(--radius-xl);
background: var(--bg-card);
box-shadow: var(--shadow-sm);
}
.table-header {
margin-bottom: 20px;
padding: 1.5rem;
border-bottom: 1px solid var(--border-light);
.table-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.table-title-group {
display: flex;
align-items: center;
gap: 12px;
gap: 1rem;
.table-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #0f172a;
color: var(--text-main);
display: flex;
align-items: center;
gap: 0.75rem;
@@ -51,175 +45,83 @@
width: 24px;
height: 24px;
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
display: flex;
flex-wrap: wrap;
gap: 2px;
padding: 2px;
background: #f1f5f9;
background: var(--bg-hover);
border-radius: 6px;
span {
width: 8px;
height: 8px;
border-radius: 2px;
background: #1353a0;
background: var(--primary-main);
opacity: 0.8;
&:nth-child(2) { opacity: 0.7; }
&:nth-child(3) { opacity: 0.4; }
&:nth-child(4) { opacity: 0.2; }
&:nth-child(2) {
opacity: 0.6;
}
&:nth-child(3) {
opacity: 0.4;
}
&:nth-child(4) {
opacity: 0.2;
}
}
}
}
.table-count {
padding: 0.2rem 0.65rem;
background: #f1f5f9;
color: #64748b;
border-radius: 12px;
padding: 0.25rem 0.75rem;
background: var(--bg-hover);
color: var(--text-muted);
border-radius: var(--radius-full);
font-size: 0.75rem;
font-weight: 500;
font-family: 'Roboto Mono', monospace;
letter-spacing: 0.02em;
}
}
/* Pill-styled filter bar */
.table-filters {
display: flex;
gap: 1rem;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 8px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
.table-search {
flex: 1 1 200px;
min-width: 180px;
max-width: 280px;
margin: 0;
}
.table-filter {
flex: 0 1 150px;
min-width: 130px;
max-width: 170px;
margin: 0;
}
/* Override Material Form Field Heights & Subscript Spacing */
::ng-deep {
.mat-mdc-form-field {
margin: 0;
}
.mat-mdc-form-field-subscript-wrapper {
display: none !important;
height: 0 !important;
}
.mat-mdc-form-field-flex {
height: 38px !important;
background: #ffffff !important;
align-items: center;
}
.mat-mdc-form-field-infix {
padding: 6px 0 !important;
min-height: 38px !important;
}
.mdc-notched-outline__leading {
border-radius: 8px 0 0 8px !important;
}
.mdc-notched-outline__trailing {
border-radius: 0 8px 8px 0 !important;
}
.mat-mdc-select-value,
.mat-mdc-select-placeholder,
input {
font-size: 13px !important;
}
}
button[mat-icon-button] {
color: #64748b;
width: 36px;
height: 36px;
&:hover {
color: #0f172a;
background: #e2e8f0;
}
mat-form-field {
width: 240px;
margin-bottom: -1.25em;
}
}
}
.table-container {
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
position: relative;
overflow: auto;
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
.table-loader {
height: 2px;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
}
}
.modern-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: transparent;
::ng-deep {
.mat-mdc-header-row {
background: #f8fafc;
min-height: 38px;
height: 38px;
}
tr.mat-row {
cursor: pointer;
transition: background-color 0.2s ease;
.mat-mdc-header-cell {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
border-bottom: 1px solid #e2e8f0;
padding: 0 16px;
white-space: nowrap;
}
.mat-mdc-row {
min-height: 42px;
height: 42px;
border-bottom: 1px solid #f1f5f9;
transition: background-color 0.15s ease;
cursor: pointer;
&:last-child {
border-bottom: none;
}
&:nth-child(even) {
background: #fafbfc;
}
&:hover {
background-color: #f1f5f9 !important;
}
}
.mat-mdc-cell {
font-size: 13px;
font-weight: 400;
color: #0f172a;
padding: 0 16px;
white-space: nowrap;
&:hover {
background-color: var(--bg-hover);
}
}
}
mat-paginator {
border-top: 1px solid #e2e8f0;
background: #ffffff;
}
@@ -1,210 +0,0 @@
<!--
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 (&#8377;)</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 }} &nbsp;|&nbsp; Account No: {{ accountNo }}</div>
</div>
</div>
</div>
@@ -1,462 +0,0 @@
/**
* 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;
}
}
@@ -1,182 +0,0 @@
/**
* 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);
});
}
}
@@ -145,16 +145,6 @@
>
{{ 'labels.inputs.Transactions' | translate }}
</a>
<a
mat-tab-link
[routerLink]="['./passbook']"
routerLinkActive
#passbook="routerLinkActive"
[active]="passbook.isActive"
*mifosxHasPermission="'READ_SHAREACCOUNTPURCHASE'"
>
Passbook
</a>
<a
mat-tab-link
[routerLink]="['./charges']"
@@ -50,10 +50,12 @@ export class SharesButtonsConfiguration {
name: 'Reject Additional Shares',
icon: 'arrow-left',
taskPermissionName: 'REJECTADDITIONAL_SHAREACCOUNT'
},
{
name: 'Redeem Shares',
icon: 'arrow-left',
taskPermissionName: 'WITHDRAW_SAVINGSACCOUNT'
}
// 'Redeem Shares' is intentionally not offered here — a share withdrawal must go
// through the Clerk Dashboard maker-checker flow (Clerk Dashboard > My Share Requests),
// never a direct one-step action from this view.
];
break;
case 'Submitted and pending approval':
@@ -54,25 +54,6 @@
</td>
</ng-container>
<ng-container matColumnDef="runningBalance">
<th mat-header-cell *matHeaderCellDef>Share Balance</th>
<td mat-cell *matCellDef="let transaction">{{ transaction.runningBalance }}</td>
</ng-container>
<ng-container matColumnDef="resolutionMeeting">
<th mat-header-cell *matHeaderCellDef>Resolution No. / Meeting Date</th>
<td mat-cell *matCellDef="let transaction">
@if (transaction.type.value === 'Redeem') {
{{ transaction.resolutionNumber || '—' }}
@if (transaction.meetingDate) {
({{ transaction.meetingDate | dateFormat }})
}
} @else {
—
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@@ -56,18 +56,14 @@ export class TransactionsTabComponent implements OnInit {
transactionsData: any;
/** Data source for transactions table. */
dataSource: MatTableDataSource<any>;
/** Columns to be displayed in transactions table — a share account's passbook: every
* purchase/redeem entry plus the running share balance and, for a withdrawal, the Board
* Resolution Number / Date of Meeting that authorised it. */
/** Columns to be displayed in transactions table. */
displayedColumns: string[] = [
'transactionDate',
'transactionType',
'totalShares',
'purchasedOrRedeemedPrice',
'chargeAmount',
'amountRecievedOrReturned',
'runningBalance',
'resolutionMeeting'
'amountRecievedOrReturned'
];
/**
@@ -77,19 +73,9 @@ export class TransactionsTabComponent implements OnInit {
constructor() {
this.route.parent.data.subscribe((data: { sharesAccountData: any }) => {
this.shareAccountData = data.sharesAccountData;
this.transactionsData = this.shareAccountData.purchasedShares
.filter((transaction: any) => transaction.type.value !== 'Charge Payment')
.slice()
.sort((a: any, b: any) => new Date(a.purchasedDate).getTime() - new Date(b.purchasedDate).getTime());
let runningBalance = 0;
this.transactionsData.forEach((transaction: any) => {
const shares = Number(transaction.numberOfShares) || 0;
runningBalance += transaction.type.value === 'Redeem' ? -shares : shares;
transaction.runningBalance = runningBalance;
});
// Passbook reads newest-first.
this.transactionsData.reverse();
this.transactionsData = this.shareAccountData.purchasedShares.filter(
(transaction: any) => transaction.type.value !== 'Charge Payment'
);
});
}
-6
View File
@@ -26,7 +26,6 @@ import { ShareAccountActionsResolver } from './common-resolvers/share-account-ac
import { SharesAccountTemplateResolver } from './common-resolvers/shares-account-template.resolver';
import { SharesAccountAndTemplateResolver } from './common-resolvers/share-account-and-template.resolver';
import { GeneralTabComponent } from './shares-account-view/general-tab/general-tab.component';
import { SharesPassbookTabComponent } from './shares-account-view/passbook-tab/passbook-tab.component';
/** Shares Routes */
const routes: Routes = [
@@ -76,11 +75,6 @@ const routes: Routes = [
component: TransactionsTabComponent,
data: { title: 'Shares Account Transactions', breadcrumb: 'Transactions', routeParamBreadcrumb: false }
},
{
path: 'passbook',
component: SharesPassbookTabComponent,
data: { title: 'Shares Account Passbook', breadcrumb: 'Passbook', routeParamBreadcrumb: false }
},
{
path: 'charges',
component: ChargesTabComponent,