feat(tasks,shares): fix Share Account Edit Details routing/auto-select, popup compare dialog, redesign Approval Center filters
- Register the missing edit-details route so the Share Account "Edit Details" maker-checker screen (already built) is actually reachable, and remove the stale hardcoded disable on its menu entry. - Fix Terms step race: Default Savings Account, Total Shares and Application Date now display correctly in edit mode instead of racing an async product-template fetch or reusing pending-application-only fields. - Show a client's linked share account number on the Savings Account General tab. - Replace the inline diff panel on Pending Share Account Edits with a popup compare dialog (mirrors the savings equivalent), showing only fields that actually changed. - Redesign Pending Share/Savings Account Edits, Share Withdrawal Requests, Dividend Requests and Share Addition Requests with a consistent header, search box and (where the data supports it) date-range filters, matching the existing Withdrawal Requests screen.
This commit is contained in:
@@ -28,10 +28,18 @@ export class ClerkShareWithdrawalService {
|
||||
return this.http.get('/clerk/share-withdrawal-requests/my', { params });
|
||||
}
|
||||
|
||||
getPendingRequests(status = 'PENDING', offset = 0, limit = 20, fromDate?: string, toDate?: string): Observable<any> {
|
||||
getPendingRequests(
|
||||
status = 'PENDING',
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
fromDate?: string,
|
||||
toDate?: string,
|
||||
search?: string
|
||||
): Observable<any> {
|
||||
let params = new HttpParams().set('status', status).set('offset', String(offset)).set('limit', String(limit));
|
||||
if (fromDate) params = params.set('fromDate', fromDate);
|
||||
if (toDate) params = params.set('toDate', toDate);
|
||||
if (search) params = params.set('search', search);
|
||||
return this.http.get('/clerk/share-withdrawal-requests', { params });
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,18 @@ export class ClerkWithdrawalService {
|
||||
return this.http.get('/clerk/withdrawal-requests/my', { params });
|
||||
}
|
||||
|
||||
getPendingRequests(status = 'PENDING', offset = 0, limit = 20, fromDate?: string, toDate?: string): Observable<any> {
|
||||
getPendingRequests(
|
||||
status = 'PENDING',
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
fromDate?: string,
|
||||
toDate?: string,
|
||||
search?: string
|
||||
): Observable<any> {
|
||||
let params = new HttpParams().set('status', status).set('offset', String(offset)).set('limit', String(limit));
|
||||
if (fromDate) params = params.set('fromDate', fromDate);
|
||||
if (toDate) params = params.set('toDate', toDate);
|
||||
if (search) params = params.set('search', search);
|
||||
return this.http.get('/clerk/withdrawal-requests', { params });
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
{{ savingsAccountData.clientName || savingsAccountData.groupName || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
@if (savingsAccountData.shareLinkingEnabled) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Linked Share Account' | translate }}</span>
|
||||
<span class="nx-row__value nx-mono">{{ savingsAccountData.shareAccountNo || 'Unassigned' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
|
||||
<div class="nx-row">
|
||||
<span class="nx-row__label">{{ 'labels.inputs.Activated On' | translate }}</span>
|
||||
|
||||
+26
-6
@@ -78,9 +78,15 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit {
|
||||
currencyCode: this.sharesAccountProductTemplate.currency.code,
|
||||
decimal: this.sharesAccountProductTemplate.currency.decimalPlaces,
|
||||
currencyMultiple: this.sharesAccountProductTemplate.currency.inMultiplesOf,
|
||||
unitPrice: this.sharesAccountProductTemplate.currentMarketPrice,
|
||||
savingsAccountId: ''
|
||||
unitPrice: this.sharesAccountProductTemplate.currentMarketPrice
|
||||
});
|
||||
// A product change clears any previously-selected savings account, since it may no longer be
|
||||
// valid for the new product's currency - but only once, and never after the account's own
|
||||
// already-linked savings account has been restored (below, and in ngOnInit for approvalEditMode) -
|
||||
// otherwise this reset races the async product-template fetch and wipes that restored value.
|
||||
if (!this.isSavingsPatched) {
|
||||
this.sharesAccountTermsForm.get('savingsAccountId').patchValue('');
|
||||
}
|
||||
this.setOptions();
|
||||
if (this.sharesAccountTemplate) {
|
||||
if (!this.isSavingsPatched && this.sharesAccountTemplate.savingsAccountId) {
|
||||
@@ -94,20 +100,34 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit {
|
||||
ngOnInit() {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
if (this.sharesAccountTemplate) {
|
||||
// In approvalEditMode the account is already active: there is nothing "pending approval" and
|
||||
// purchasedShares[0] is a transaction row, not the application date - use the account's real
|
||||
// approved share count and its original submitted date instead (both fields are disabled below
|
||||
// and never sent in the edit payload, so this is display-only).
|
||||
this.sharesAccountTermsForm.patchValue({
|
||||
requestedShares: this.sharesAccountTemplate.summary.totalPendingForApprovalShares,
|
||||
requestedShares: this.approvalEditMode
|
||||
? this.sharesAccountTemplate.summary.totalApprovedShares
|
||||
: this.sharesAccountTemplate.summary.totalPendingForApprovalShares,
|
||||
minimumActivePeriod: this.sharesAccountTemplate.minimumActivePeriod,
|
||||
minimumActivePeriodFrequencyType:
|
||||
this.sharesAccountTemplate.minimumActivePeriod && this.sharesAccountTemplate.minimumActivePeriodTypeEnum.id,
|
||||
lockinPeriodFrequency: this.sharesAccountTemplate.lockinPeriod,
|
||||
lockinPeriodFrequencyType:
|
||||
this.sharesAccountTemplate.lockinPeriod && this.sharesAccountTemplate.lockPeriodTypeEnum.id,
|
||||
applicationDate:
|
||||
this.sharesAccountTemplate.purchasedShares[0].purchasedDate &&
|
||||
new Date(this.sharesAccountTemplate.purchasedShares[0].purchasedDate),
|
||||
applicationDate: this.approvalEditMode
|
||||
? this.sharesAccountTemplate.timeline.submittedOnDate &&
|
||||
new Date(this.sharesAccountTemplate.timeline.submittedOnDate)
|
||||
: this.sharesAccountTemplate.purchasedShares[0].purchasedDate &&
|
||||
new Date(this.sharesAccountTemplate.purchasedShares[0].purchasedDate),
|
||||
allowDividendCalculationForInactiveClients:
|
||||
this.sharesAccountTemplate.allowDividendCalculationForInactiveClients
|
||||
});
|
||||
// Seeded directly here rather than left to the async product-template fetch in ngOnChanges:
|
||||
// that fetch can resolve out of order and its own reset-to-'' would otherwise win the race.
|
||||
if (!this.isSavingsPatched && this.sharesAccountTemplate.savingsAccountId) {
|
||||
this.sharesAccountTermsForm.get('savingsAccountId').patchValue(this.sharesAccountTemplate.savingsAccountId);
|
||||
this.isSavingsPatched = true;
|
||||
}
|
||||
}
|
||||
if (this.approvalEditMode) {
|
||||
this.sharesAccountTermsForm.get('requestedShares').disable();
|
||||
|
||||
@@ -105,15 +105,7 @@
|
||||
<mat-menu #More="matMenu">
|
||||
@for (option of buttonConfig.options; track option) {
|
||||
<span>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="option.taskPermissionName"
|
||||
[disabled]="option.name === 'Edit Details'"
|
||||
[matTooltip]="
|
||||
option.name === 'Edit Details' ? ('tooltips.Editing details is disabled' | translate) : ''
|
||||
"
|
||||
(click)="doAction(option.name)"
|
||||
>
|
||||
<button mat-menu-item *mifosxHasPermission="option.taskPermissionName" (click)="doAction(option.name)">
|
||||
{{ 'labels.menus.' + option.name | translate }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ChargesTabComponent } from './shares-account-view/charges-tab/charges-t
|
||||
import { DividendsTabComponent } from './shares-account-view/dividends-tab/dividends-tab.component';
|
||||
import { CreateSharesAccountComponent } from './create-shares-account/create-shares-account.component';
|
||||
import { EditSharesAccountComponent } from './edit-shares-account/edit-shares-account.component';
|
||||
import { EditShareAccountDetailsComponent } from './edit-share-account-details/edit-share-account-details.component';
|
||||
import { SharesAccountActionsComponent } from './shares-account-actions/shares-account-actions.component';
|
||||
import { ShareHoldersComponent } from './share-holders/share-holders.component';
|
||||
|
||||
@@ -108,6 +109,14 @@ const routes: Routes = [
|
||||
sharesAccountAndTemplate: SharesAccountAndTemplateResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'edit-details',
|
||||
data: { title: 'Edit Share Account Details', breadcrumb: 'Edit Details', routeParamBreadcrumb: false },
|
||||
component: EditShareAccountDetailsComponent,
|
||||
resolve: {
|
||||
sharesAccountAndTemplate: SharesAccountAndTemplateResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'actions/:name',
|
||||
data: { title: 'Shares Account Actions', breadcrumb: 'Actions', routeParamBreadcrumb: 'name' },
|
||||
|
||||
+93
-55
@@ -6,63 +6,101 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Dividend Requests — Declared, Awaiting Approval</div>
|
||||
<div class="inv-form-grid" style="padding: 12px 16px">
|
||||
<label class="inv-form-field" style="align-self: end">
|
||||
<button type="button" class="pg-btn" [disabled]="loading" (click)="load()">Refresh</button>
|
||||
</label>
|
||||
<div class="dr-tab">
|
||||
<!-- ── TITLE BAR ── -->
|
||||
<div class="dr-action-bar">
|
||||
<div class="dr-action-bar__left">
|
||||
<mat-icon class="dr-action-bar__icon">payments</mat-icon>
|
||||
<div>
|
||||
<div class="dr-action-bar__title">Dividend Requests</div>
|
||||
<div class="dr-action-bar__sub">Every Share Product dividend declaration still awaiting approval.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Share Product</th>
|
||||
<th>Dividend Period</th>
|
||||
<th class="num">Total Amount</th>
|
||||
<th class="num">Members</th>
|
||||
<th>Declared On</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading) {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">Loading…</td>
|
||||
</tr>
|
||||
} @else if (loaded && rows.length === 0) {
|
||||
<tr>
|
||||
<td colspan="6" class="pg-empty">No dividend declarations are waiting for approval.</td>
|
||||
</tr>
|
||||
} @else {
|
||||
<!-- The whole row opens the member-by-member breakdown, so a checker can get to the detail
|
||||
the same way they would on the Dividends list rather than having to find the View
|
||||
button. The action buttons stop propagation so Approve still approves directly. -->
|
||||
@for (row of rows; track row.dividendId) {
|
||||
<tr class="dr-row" (click)="openView(row)">
|
||||
<td>{{ row.productName }}</td>
|
||||
<td>{{ row.dividendPeriodStartDate }} → {{ row.dividendPeriodEndDate }}</td>
|
||||
<td class="num">{{ row.amount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ row.memberCount }}</td>
|
||||
<td>{{ row.declaredOn | slice: 0 : 10 }}</td>
|
||||
<td class="dr-actions">
|
||||
<button type="button" class="pg-btn" (click)="openView(row); $event.stopPropagation()">View</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn dr-approve-btn"
|
||||
[disabled]="approvingId === row.dividendId"
|
||||
(click)="approve(row); $event.stopPropagation()"
|
||||
>
|
||||
{{ approvingId === row.dividendId ? 'Approving…' : 'Approve' }}
|
||||
</button>
|
||||
@if (approveError[row.dividendId]) {
|
||||
<div class="dr-approve-error">{{ approveError[row.dividendId] }}</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
<!-- ── FILTER BAR ── -->
|
||||
<div class="dr-filter-bar">
|
||||
<div class="dr-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by share product"
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="applyFilter()"
|
||||
/>
|
||||
@if (searchTerm) {
|
||||
<button type="button" class="dr-search-box__clear" (click)="searchTerm = ''; applyFilter()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dr-btn dr-btn--refresh" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── TABLE ── -->
|
||||
@if (loading) {
|
||||
<div class="empty-state">
|
||||
<p class="empty-title">Loading…</p>
|
||||
</div>
|
||||
} @else if (loaded && rows.length === 0) {
|
||||
<div class="empty-state">
|
||||
<mat-icon class="empty-icon">check_circle</mat-icon>
|
||||
<p class="empty-title">No dividend declarations awaiting approval</p>
|
||||
<p class="empty-text">
|
||||
@if (allRows.length > 0) {
|
||||
No declarations match "{{ searchTerm }}".
|
||||
} @else {
|
||||
Every declared dividend has already been actioned.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dr-table-wrap">
|
||||
<table class="dr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Share Product</th>
|
||||
<th>Dividend Period</th>
|
||||
<th class="num">Total Amount</th>
|
||||
<th class="num">Members</th>
|
||||
<th>Declared On</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- The whole row opens the member-by-member breakdown, so a checker can get to the detail
|
||||
the same way they would on the Dividends list rather than having to find the View
|
||||
button. The action buttons stop propagation so Approve still approves directly. -->
|
||||
@for (row of rows; track row.dividendId) {
|
||||
<tr class="dr-row" (click)="openView(row)">
|
||||
<td>{{ row.productName }}</td>
|
||||
<td class="dr-dt">{{ row.dividendPeriodStartDate }} → {{ row.dividendPeriodEndDate }}</td>
|
||||
<td class="num dr-amount">{{ row.amount | number: '1.2-2' }}</td>
|
||||
<td class="num">{{ row.memberCount }}</td>
|
||||
<td class="dr-dt">{{ row.declaredOn | slice: 0 : 10 }}</td>
|
||||
<td class="dr-actions" (click)="$event.stopPropagation()">
|
||||
<button type="button" class="dr-btn" (click)="openView(row)">View</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dr-btn dr-approve-btn"
|
||||
[disabled]="approvingId === row.dividendId"
|
||||
(click)="approve(row)"
|
||||
>
|
||||
{{ approvingId === row.dividendId ? 'Approving…' : 'Approve' }}
|
||||
</button>
|
||||
@if (approveError[row.dividendId]) {
|
||||
<div class="dr-approve-error">{{ approveError[row.dividendId] }}</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showViewModal && viewProductId && viewDividendId) {
|
||||
|
||||
+241
-9
@@ -6,6 +6,209 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dr-tab {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// ── Title bar — mirrors WithdrawalRequestsComponent's action bar ──────────
|
||||
.dr-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter bar ──────────────────────────────────────────────────────────
|
||||
.dr-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 20px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dr-search-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
min-width: 260px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 0 0 3px rgb(15 79 60 / 8%);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dr-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 7px;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: background 0.15s;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
color: #0d1b2a;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--refresh {
|
||||
padding: 0 8px;
|
||||
color: #64748b;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dr-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.dr-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #f8fafc;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.dr-dt {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dr-amount {
|
||||
font-weight: 700;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
.dr-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -18,6 +221,10 @@
|
||||
color: #fff;
|
||||
border-color: #2563eb;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -30,6 +237,40 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: #94a3b8;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 40px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* The whole row opens the member-by-member breakdown, so it has to look like it will. */
|
||||
.dr-row {
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover td {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dr-popup-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -83,12 +324,3 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* The whole row opens the member-by-member breakdown, so it has to look like it will. */
|
||||
.dr-row {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -8,7 +8,9 @@
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@@ -33,6 +35,8 @@ import { ViewDividendComponent } from '../../../products/share-products/view-div
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatIconModule,
|
||||
ViewDividendComponent
|
||||
]
|
||||
})
|
||||
@@ -42,9 +46,13 @@ export class DividendRequestsComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
|
||||
/** Raw, unfiltered list from the API -- search happens client-side over this since the endpoint
|
||||
* has no server-side filtering (it always returns every pending declaration in one shot). */
|
||||
allRows: any[] = [];
|
||||
rows: any[] = [];
|
||||
loading = false;
|
||||
loaded = false;
|
||||
searchTerm = '';
|
||||
|
||||
approvingId: number | null = null;
|
||||
approveError: Record<number, string> = {};
|
||||
@@ -106,12 +114,20 @@ export class DividendRequestsComponent implements OnInit {
|
||||
.list()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((res: any[]) => {
|
||||
this.rows = res ?? [];
|
||||
this.allRows = res ?? [];
|
||||
this.applyFilter();
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(): void {
|
||||
const needle = this.searchTerm.trim().toLowerCase();
|
||||
this.rows = !needle
|
||||
? this.allRows
|
||||
: this.allRows.filter((row) => `${row.productName ?? ''}`.toLowerCase().includes(needle));
|
||||
}
|
||||
|
||||
approve(row: any): void {
|
||||
this.approvingId = row.dividendId;
|
||||
delete this.approveError[row.dividendId];
|
||||
|
||||
+159
-72
@@ -7,88 +7,175 @@
|
||||
-->
|
||||
|
||||
<div class="psae-tab-content">
|
||||
<!-- ── TITLE BAR ── -->
|
||||
<div class="psae-header">
|
||||
<div class="psae-header__left">
|
||||
<mat-icon class="psae-header__icon">edit_note</mat-icon>
|
||||
<div>
|
||||
<div class="psae-header__title">Pending Account Edits</div>
|
||||
<div class="psae-header__sub">Review, approve or reject proposed Nominee / Digital-Feature edits.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (showData) {
|
||||
<div class="psae-action-bar">
|
||||
<mat-form-field appearance="outline" class="psae-search-field">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input #filterInput matInput placeholder="Search by account number" (keyup)="applyFilter(filterInput.value)" />
|
||||
</mat-form-field>
|
||||
<!-- ── FILTER BAR ── -->
|
||||
<div class="psae-filter-bar">
|
||||
<div class="psae-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
#filterInput
|
||||
type="text"
|
||||
placeholder="Search by account, client or submitter"
|
||||
[value]="searchTerm"
|
||||
(input)="applyFilter(filterInput.value)"
|
||||
/>
|
||||
@if (searchTerm) {
|
||||
<button type="button" class="psae-search-box__clear" (click)="filterInput.value = ''; applyFilter('')">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="psae-fb-sep"></span>
|
||||
|
||||
<div class="psae-presets">
|
||||
@for (p of datePresets; track p.key) {
|
||||
<button
|
||||
type="button"
|
||||
class="psae-preset-chip"
|
||||
[class.psae-preset-chip--active]="activePreset === p.key"
|
||||
(click)="applyPreset(p.key)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="psae-fb-sep"></span>
|
||||
|
||||
<div class="psae-date-range">
|
||||
<div class="psae-date-box">
|
||||
<span class="psae-date-box__lbl">From</span>
|
||||
<input
|
||||
class="psae-date-box__input"
|
||||
[matDatepicker]="fromPicker"
|
||||
[(ngModel)]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onFromDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="psae-date-box__btn" (click)="fromPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</div>
|
||||
<span class="psae-date-range__sep">—</span>
|
||||
<div class="psae-date-box">
|
||||
<span class="psae-date-box__lbl">To</span>
|
||||
<input
|
||||
class="psae-date-box__input"
|
||||
[matDatepicker]="toPicker"
|
||||
[(ngModel)]="toDate"
|
||||
[min]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onToDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="psae-date-box__btn" (click)="toPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="psae-btn psae-btn--refresh" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="modern-table">
|
||||
<ng-container matColumnDef="accountNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Account No.</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.accountNo }}</td>
|
||||
</ng-container>
|
||||
@if (dataSource.data.length === 0) {
|
||||
<div class="empty-state psae-empty-inline">
|
||||
<fa-icon icon="filter" class="empty-icon"></fa-icon>
|
||||
<p class="empty-title">No matching requests</p>
|
||||
<p class="empty-text">No pending edits match the current search or date range.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="psae-table">
|
||||
<ng-container matColumnDef="accountNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Account No.</th>
|
||||
<td mat-cell *matCellDef="let edit" class="cell-mono">{{ edit.accountNo }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="accountType">
|
||||
<th mat-header-cell *matHeaderCellDef>Account Type</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span
|
||||
class="status-badge"
|
||||
[class.status-badge--savings]="edit.accountType === 'Savings'"
|
||||
[class.status-badge--current]="edit.accountType === 'Current'"
|
||||
[class.status-badge--fd]="edit.accountType === 'Fixed Deposit'"
|
||||
[class.status-badge--rd]="edit.accountType === 'Recurring Deposit'"
|
||||
>
|
||||
{{ edit.accountType }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="accountType">
|
||||
<th mat-header-cell *matHeaderCellDef>Account Type</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span
|
||||
class="status-badge"
|
||||
[class.status-badge--savings]="edit.accountType === 'Savings'"
|
||||
[class.status-badge--current]="edit.accountType === 'Current'"
|
||||
[class.status-badge--fd]="edit.accountType === 'Fixed Deposit'"
|
||||
[class.status-badge--rd]="edit.accountType === 'Recurring Deposit'"
|
||||
>
|
||||
{{ edit.accountType }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="makerUsername">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.makerUsername }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="makerUsername">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.makerUsername }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="createdOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ formatDate(edit.createdOn) }}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="createdOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let edit" class="psae-dt">{{ formatDate(edit.createdOn) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="holdAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>Hold Amount</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
@if (edit.holdAmount) {
|
||||
<span class="status-badge status-badge--hold">{{ edit.holdAmount }}</span>
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="holdAmount">
|
||||
<th mat-header-cell *matHeaderCellDef>Hold Amount</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
@if (edit.holdAmount) {
|
||||
<span class="status-badge status-badge--hold">{{ edit.holdAmount }}</span>
|
||||
} @else {
|
||||
—
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span class="status-badge status-badge--pending">{{ edit.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span class="status-badge status-badge--pending">{{ edit.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psae-btn psae-btn-view"
|
||||
[disabled]="loadingCompareFor === edit"
|
||||
(click)="viewCompare(edit)"
|
||||
>
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>{{ loadingCompareFor === edit ? 'Loading…' : 'Compare' }}
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-reject" (click)="reject(edit)">
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psae-btn psae-btn-view"
|
||||
[disabled]="loadingCompareFor === edit"
|
||||
(click)="viewCompare(edit)"
|
||||
>
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>{{ loadingCompareFor === edit ? 'Loading…' : 'Compare' }}
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-reject" (click)="reject(edit)">
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="psae-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!showData) {
|
||||
|
||||
+320
-8
@@ -10,21 +10,298 @@ $border: #e2e8f0;
|
||||
$text-muted: #64748b;
|
||||
|
||||
.psae-tab-content {
|
||||
padding: 16px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.psae-action-bar {
|
||||
// ── Title bar — mirrors WithdrawalRequestsComponent's action bar ──────────
|
||||
.psae-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid $border;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter bar + search box — mirrors WithdrawalRequestsComponent ─────────
|
||||
.psae-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 20px 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.psae-search-field {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
.psae-fb-sep {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
background: $border;
|
||||
flex-shrink: 0;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.psae-search-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
min-width: 260px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 0 0 3px rgb(15 79 60 / 8%);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.psae-presets {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.psae-preset-chip {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid $border;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: #0f4f3c;
|
||||
color: #0f4f3c;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #0f4f3c;
|
||||
border-color: #0f4f3c;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #0a3d2e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.psae-date-range {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&__sep {
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
padding: 0 2px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-date-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 34px;
|
||||
padding: 0 6px 0 10px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
&__input {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
width: 76px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&::placeholder {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #0f4f3c;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
// ── Table — mirrors WithdrawalRequestsComponent's table ───────────────────
|
||||
.psae-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: $text-muted;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #f8fafc;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-row {
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover td {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-dt {
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.psae-btn {
|
||||
@@ -41,6 +318,36 @@ $text-muted: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn--refresh {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
margin-right: 0;
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid $border;
|
||||
background: #fff;
|
||||
color: $text-muted;
|
||||
cursor: pointer;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn-view {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
@@ -117,7 +424,12 @@ $text-muted: #64748b;
|
||||
border: 1px dashed $border;
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
margin: 16px 20px;
|
||||
|
||||
&.psae-empty-inline {
|
||||
padding: 40px 24px;
|
||||
margin: 0 20px 16px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 42px;
|
||||
|
||||
+136
-26
@@ -8,6 +8,8 @@
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import {
|
||||
MatTableDataSource,
|
||||
MatTable,
|
||||
@@ -23,7 +25,6 @@ import {
|
||||
} from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { SavingsService } from 'app/savings/savings.service';
|
||||
@@ -45,6 +46,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./pending-savings-account-edits.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
@@ -56,8 +58,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatIcon,
|
||||
MatIconButton
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class PendingSavingsAccountEditsComponent {
|
||||
@@ -82,36 +83,145 @@ export class PendingSavingsAccountEditsComponent {
|
||||
|
||||
/** Set only while the current account is being fetched, so the button can show progress. */
|
||||
loadingCompareFor: any = null;
|
||||
loading = false;
|
||||
|
||||
/** Raw, unfiltered list from the resolvers -- filtering happens client-side over this since both
|
||||
* endpoints always return every PENDING edit in one shot (no server-side paging). */
|
||||
private allItems: any[] = [];
|
||||
|
||||
searchTerm = '';
|
||||
fromDate: Date | null = null;
|
||||
toDate: Date | null = null;
|
||||
maxDate!: Date;
|
||||
activePreset = 'all';
|
||||
|
||||
readonly datePresets = [
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'week', label: 'This Week' },
|
||||
{ key: 'month', label: 'This Month' },
|
||||
{ key: 'all', label: 'All' }
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.maxDate = new Date();
|
||||
this.route.data.subscribe((data: { pendingSavingsAccountEditsData: any; pendingCurrentAccountEditsData: any }) => {
|
||||
const savingsItems = (data.pendingSavingsAccountEditsData || []).map((item: any) => {
|
||||
let type = 'Savings';
|
||||
if (item.depositAccountType === 'FIXED_DEPOSIT') {
|
||||
type = 'Fixed Deposit';
|
||||
} else if (item.depositAccountType === 'RECURRING_DEPOSIT') {
|
||||
type = 'Recurring Deposit';
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
accountType: type
|
||||
};
|
||||
});
|
||||
const currentItems = (data.pendingCurrentAccountEditsData || []).map((item: any) => ({
|
||||
...item,
|
||||
accountType: 'Current'
|
||||
}));
|
||||
const items = [
|
||||
...savingsItems,
|
||||
...currentItems
|
||||
];
|
||||
this.showData = items.length > 0;
|
||||
this.dataSource = new MatTableDataSource(items);
|
||||
this.allItems = this.mergeItems(data.pendingSavingsAccountEditsData, data.pendingCurrentAccountEditsData);
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
private mergeItems(savingsData: any, currentData: any): any[] {
|
||||
const savingsItems = (savingsData || []).map((item: any) => {
|
||||
let type = 'Savings';
|
||||
if (item.depositAccountType === 'FIXED_DEPOSIT') {
|
||||
type = 'Fixed Deposit';
|
||||
} else if (item.depositAccountType === 'RECURRING_DEPOSIT') {
|
||||
type = 'Recurring Deposit';
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
accountType: type
|
||||
};
|
||||
});
|
||||
const currentItems = (currentData || []).map((item: any) => ({
|
||||
...item,
|
||||
accountType: 'Current'
|
||||
}));
|
||||
return [
|
||||
...savingsItems,
|
||||
...currentItems
|
||||
];
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string = '') {
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
this.searchTerm = filterValue;
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
applyPreset(key: string): void {
|
||||
this.activePreset = key;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (key === 'today') {
|
||||
this.fromDate = new Date(today);
|
||||
this.toDate = new Date(today);
|
||||
} else if (key === 'week') {
|
||||
const start = new Date(today);
|
||||
start.setDate(today.getDate() - today.getDay());
|
||||
this.fromDate = start;
|
||||
this.toDate = new Date(today);
|
||||
} else if (key === 'month') {
|
||||
this.fromDate = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
this.toDate = new Date(today);
|
||||
} else {
|
||||
this.fromDate = null;
|
||||
this.toDate = null;
|
||||
}
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onFromDateChange(): void {
|
||||
this.activePreset = '';
|
||||
if (this.fromDate && this.toDate && this.fromDate > this.toDate) {
|
||||
this.toDate = new Date(this.fromDate);
|
||||
}
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onToDateChange(): void {
|
||||
this.activePreset = '';
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
private itemDate(item: any): Date | null {
|
||||
const raw = item?.createdOn;
|
||||
if (!raw) return null;
|
||||
const d = Array.isArray(raw) ? new Date(raw[0], raw[1] - 1, raw[2], raw[3] || 0, raw[4] || 0) : new Date(raw);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
private applyFilters(): void {
|
||||
const from = this.fromDate
|
||||
? new Date(this.fromDate.getFullYear(), this.fromDate.getMonth(), this.fromDate.getDate())
|
||||
: null;
|
||||
const to = this.toDate
|
||||
? new Date(this.toDate.getFullYear(), this.toDate.getMonth(), this.toDate.getDate(), 23, 59, 59, 999)
|
||||
: null;
|
||||
const needle = this.searchTerm.trim().toLowerCase();
|
||||
|
||||
const filtered = this.allItems.filter((item) => {
|
||||
if (from || to) {
|
||||
const d = this.itemDate(item);
|
||||
if (!d) return false;
|
||||
if (from && d < from) return false;
|
||||
if (to && d > to) return false;
|
||||
}
|
||||
if (needle) {
|
||||
const haystack = `${item.accountNo ?? ''} ${item.clientName ?? ''} ${item.makerUsername ?? ''}`.toLowerCase();
|
||||
if (!haystack.includes(needle)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.showData = this.allItems.length > 0;
|
||||
this.dataSource = new MatTableDataSource(filtered);
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
forkJoin({
|
||||
savings: this.savingsService.getAllPendingSavingsAccountEdits(),
|
||||
current: this.currentAccountsService.getAllPendingCurrentAccountEdits()
|
||||
}).subscribe({
|
||||
next: ({ savings, current }) => {
|
||||
this.allItems = this.mergeItems(savings, current);
|
||||
this.applyFilters();
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatDate(date: any): string {
|
||||
|
||||
+144
-85
@@ -7,94 +7,153 @@
|
||||
-->
|
||||
|
||||
<div class="psae-tab-content">
|
||||
<!-- ── TITLE BAR ── -->
|
||||
<div class="psae-header">
|
||||
<div class="psae-header__left">
|
||||
<mat-icon class="psae-header__icon">edit_note</mat-icon>
|
||||
<div>
|
||||
<div class="psae-header__title">Pending Share Account Edits</div>
|
||||
<div class="psae-header__sub">Review, approve or reject proposed Share Account detail edits.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (showData) {
|
||||
<div class="psae-action-bar">
|
||||
<mat-form-field appearance="outline" class="psae-search-field">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input #filterInput matInput placeholder="Search by account number" (keyup)="applyFilter(filterInput.value)" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="modern-table">
|
||||
<ng-container matColumnDef="accountNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Account No.</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.accountNo }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="clientName">
|
||||
<th mat-header-cell *matHeaderCellDef>Client</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.clientName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="makerUsername">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.makerUsername }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="createdOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ formatDate(edit.createdOn) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span class="status-badge status-badge--pending">{{ edit.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<button mat-stroked-button class="psae-btn psae-btn-view" (click)="viewCompare(edit)">
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>Compare
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-reject" (click)="reject(edit)">
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (selectedEdit) {
|
||||
<div class="psae-diff-panel">
|
||||
<div class="psae-diff-header">
|
||||
<h3>Proposed Changes — Account "{{ selectedEdit.accountNo }}"</h3>
|
||||
<button mat-icon-button (click)="closeCompare()"><fa-icon icon="times"></fa-icon></button>
|
||||
</div>
|
||||
|
||||
<div class="psae-diff-block">
|
||||
<h4>Details</h4>
|
||||
<table class="psae-diff-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Proposed Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of entries(selectedEdit); track row.key) {
|
||||
<tr>
|
||||
<td class="psae-diff-label">{{ row.key }}</td>
|
||||
<td class="psae-diff-proposed">{{ row.value }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (selectedEdit.rejectReason) {
|
||||
<p class="psae-reject-reason"><strong>Reject Reason:</strong> {{ selectedEdit.rejectReason }}</p>
|
||||
<!-- ── FILTER BAR ── -->
|
||||
<div class="psae-filter-bar">
|
||||
<div class="psae-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
#filterInput
|
||||
type="text"
|
||||
placeholder="Search by account, client or submitter"
|
||||
[value]="searchTerm"
|
||||
(input)="applyFilter(filterInput.value)"
|
||||
/>
|
||||
@if (searchTerm) {
|
||||
<button type="button" class="psae-search-box__clear" (click)="filterInput.value = ''; applyFilter('')">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="psae-fb-sep"></span>
|
||||
|
||||
<div class="psae-presets">
|
||||
@for (p of datePresets; track p.key) {
|
||||
<button
|
||||
type="button"
|
||||
class="psae-preset-chip"
|
||||
[class.psae-preset-chip--active]="activePreset === p.key"
|
||||
(click)="applyPreset(p.key)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="psae-fb-sep"></span>
|
||||
|
||||
<div class="psae-date-range">
|
||||
<div class="psae-date-box">
|
||||
<span class="psae-date-box__lbl">From</span>
|
||||
<input
|
||||
class="psae-date-box__input"
|
||||
[matDatepicker]="fromPicker"
|
||||
[(ngModel)]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onFromDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="psae-date-box__btn" (click)="fromPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</div>
|
||||
<span class="psae-date-range__sep">—</span>
|
||||
<div class="psae-date-box">
|
||||
<span class="psae-date-box__lbl">To</span>
|
||||
<input
|
||||
class="psae-date-box__input"
|
||||
[matDatepicker]="toPicker"
|
||||
[(ngModel)]="toDate"
|
||||
[min]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onToDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="psae-date-box__btn" (click)="toPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="psae-btn psae-btn--refresh" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (dataSource.data.length === 0) {
|
||||
<div class="empty-state psae-empty-inline">
|
||||
<fa-icon icon="filter" class="empty-icon"></fa-icon>
|
||||
<p class="empty-title">No matching requests</p>
|
||||
<p class="empty-text">No pending edits match the current search or date range.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="psae-table">
|
||||
<ng-container matColumnDef="accountNo">
|
||||
<th mat-header-cell *matHeaderCellDef>Account No.</th>
|
||||
<td mat-cell *matCellDef="let edit" class="cell-mono">{{ edit.accountNo }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="clientName">
|
||||
<th mat-header-cell *matHeaderCellDef>Client</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.clientName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="makerUsername">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted By</th>
|
||||
<td mat-cell *matCellDef="let edit">{{ edit.makerUsername }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="createdOn">
|
||||
<th mat-header-cell *matHeaderCellDef>Submitted On</th>
|
||||
<td mat-cell *matCellDef="let edit" class="psae-dt">{{ formatDate(edit.createdOn) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<span class="status-badge status-badge--pending">{{ edit.status }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let edit">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="psae-btn psae-btn-view"
|
||||
[disabled]="loadingCompareFor === edit"
|
||||
(click)="viewCompare(edit)"
|
||||
>
|
||||
<fa-icon icon="eye" class="m-r-10"></fa-icon>{{ loadingCompareFor === edit ? 'Loading…' : 'Compare' }}
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-approve" (click)="approve(edit)">
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>Approve
|
||||
</button>
|
||||
<button mat-stroked-button class="psae-btn psae-btn-reject" (click)="reject(edit)">
|
||||
<fa-icon icon="ban" class="m-r-10"></fa-icon>Reject
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="psae-row"></tr>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+320
-81
@@ -10,21 +10,298 @@ $border: #e2e8f0;
|
||||
$text-muted: #64748b;
|
||||
|
||||
.psae-tab-content {
|
||||
padding: 16px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.psae-action-bar {
|
||||
// ── Title bar — mirrors WithdrawalRequestsComponent's action bar ──────────
|
||||
.psae-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid $border;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter bar + search box — mirrors WithdrawalRequestsComponent ─────────
|
||||
.psae-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 20px 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.psae-search-field {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
.psae-fb-sep {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
background: $border;
|
||||
flex-shrink: 0;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.psae-presets {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.psae-preset-chip {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid $border;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: #0f4f3c;
|
||||
color: #0f4f3c;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #0f4f3c;
|
||||
border-color: #0f4f3c;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #0a3d2e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.psae-date-range {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&__sep {
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
padding: 0 2px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-date-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 34px;
|
||||
padding: 0 6px 0 10px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
&__input {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
width: 76px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&::placeholder {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #0f4f3c;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.psae-search-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
min-width: 260px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 0 0 3px rgb(15 79 60 / 8%);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
// ── Table — mirrors WithdrawalRequestsComponent's table ───────────────────
|
||||
.psae-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: $text-muted;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #f8fafc;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-row {
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover td {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-dt {
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.psae-btn {
|
||||
@@ -41,6 +318,36 @@ $text-muted: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn--refresh {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
margin-right: 0;
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid $border;
|
||||
background: #fff;
|
||||
color: $text-muted;
|
||||
cursor: pointer;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-btn-view {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
@@ -82,79 +389,6 @@ $text-muted: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-panel {
|
||||
margin-top: 20px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.psae-diff-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-block {
|
||||
margin-top: 16px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-loading {
|
||||
color: $text-muted;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.psae-diff-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 4px;
|
||||
font-size: 12.5px;
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $text-muted;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.psae-diff-label {
|
||||
font-weight: 600;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
.psae-diff-proposed {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.psae-reject-reason {
|
||||
margin-top: 14px;
|
||||
font-size: 12.5px;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -165,7 +399,7 @@ $text-muted: #64748b;
|
||||
border: 1px dashed $border;
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
margin: 16px 20px;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 42px;
|
||||
@@ -180,6 +414,11 @@ $text-muted: #64748b;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
&.psae-empty-inline {
|
||||
padding: 40px 24px;
|
||||
margin: 0 20px 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
|
||||
+133
-50
@@ -8,6 +8,7 @@
|
||||
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
MatTableDataSource,
|
||||
MatTable,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
} from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { SharesService } from 'app/shares/shares.service';
|
||||
@@ -32,6 +32,7 @@ import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.componen
|
||||
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
||||
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { ShareAccountEditCompareDialogComponent } from './share-account-edit-compare-dialog/share-account-edit-compare-dialog.component';
|
||||
|
||||
/**
|
||||
* Checker-facing list of Share Account details edits awaiting approval.
|
||||
@@ -43,6 +44,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./pending-share-account-edits.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
@@ -54,8 +56,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
MatHeaderRow,
|
||||
MatRowDef,
|
||||
MatRow,
|
||||
MatIcon,
|
||||
MatIconButton
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class PendingShareAccountEditsComponent {
|
||||
@@ -76,18 +77,120 @@ export class PendingShareAccountEditsComponent {
|
||||
'actions'
|
||||
];
|
||||
|
||||
selectedEdit: any = null;
|
||||
loadingCompareFor: any = null;
|
||||
loading = false;
|
||||
|
||||
/** Raw, unfiltered list from the resolver -- filtering happens client-side over this since the
|
||||
* endpoint always returns every PENDING edit in one shot (no server-side paging). */
|
||||
private allItems: any[] = [];
|
||||
|
||||
searchTerm = '';
|
||||
fromDate: Date | null = null;
|
||||
toDate: Date | null = null;
|
||||
maxDate!: Date;
|
||||
activePreset = 'all';
|
||||
|
||||
readonly datePresets = [
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'week', label: 'This Week' },
|
||||
{ key: 'month', label: 'This Month' },
|
||||
{ key: 'all', label: 'All' }
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.maxDate = new Date();
|
||||
this.route.data.subscribe((data: { pendingShareAccountEditsData: any }) => {
|
||||
const items = data.pendingShareAccountEditsData || [];
|
||||
this.showData = items.length > 0;
|
||||
this.dataSource = new MatTableDataSource(items);
|
||||
this.allItems = data.pendingShareAccountEditsData || [];
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string = '') {
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
this.searchTerm = filterValue;
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
applyPreset(key: string): void {
|
||||
this.activePreset = key;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (key === 'today') {
|
||||
this.fromDate = new Date(today);
|
||||
this.toDate = new Date(today);
|
||||
} else if (key === 'week') {
|
||||
const start = new Date(today);
|
||||
start.setDate(today.getDate() - today.getDay());
|
||||
this.fromDate = start;
|
||||
this.toDate = new Date(today);
|
||||
} else if (key === 'month') {
|
||||
this.fromDate = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
this.toDate = new Date(today);
|
||||
} else {
|
||||
this.fromDate = null;
|
||||
this.toDate = null;
|
||||
}
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onFromDateChange(): void {
|
||||
this.activePreset = '';
|
||||
if (this.fromDate && this.toDate && this.fromDate > this.toDate) {
|
||||
this.toDate = new Date(this.fromDate);
|
||||
}
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
onToDateChange(): void {
|
||||
this.activePreset = '';
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
private itemDate(item: any): Date | null {
|
||||
const raw = item?.createdOn;
|
||||
if (!raw) return null;
|
||||
const d = Array.isArray(raw) ? new Date(raw[0], raw[1] - 1, raw[2], raw[3] || 0, raw[4] || 0) : new Date(raw);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
private applyFilters(): void {
|
||||
const from = this.fromDate
|
||||
? new Date(this.fromDate.getFullYear(), this.fromDate.getMonth(), this.fromDate.getDate())
|
||||
: null;
|
||||
const to = this.toDate
|
||||
? new Date(this.toDate.getFullYear(), this.toDate.getMonth(), this.toDate.getDate(), 23, 59, 59, 999)
|
||||
: null;
|
||||
const needle = this.searchTerm.trim().toLowerCase();
|
||||
|
||||
const filtered = this.allItems.filter((item) => {
|
||||
if (from || to) {
|
||||
const d = this.itemDate(item);
|
||||
if (!d) return false;
|
||||
if (from && d < from) return false;
|
||||
if (to && d > to) return false;
|
||||
}
|
||||
if (needle) {
|
||||
const haystack = `${item.accountNo ?? ''} ${item.clientName ?? ''} ${item.makerUsername ?? ''}`.toLowerCase();
|
||||
if (!haystack.includes(needle)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.showData = this.allItems.length > 0;
|
||||
this.dataSource = new MatTableDataSource(filtered);
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.sharesService.getAllPendingShareAccountEdits().subscribe({
|
||||
next: (items: any) => {
|
||||
this.allItems = items || [];
|
||||
this.applyFilters();
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatDate(date: any): string {
|
||||
@@ -98,48 +201,29 @@ export class PendingShareAccountEditsComponent {
|
||||
return isNaN(d.getTime()) ? String(date) : d.toLocaleString('en-IN');
|
||||
}
|
||||
|
||||
/** Readable list of "key: value" pairs for the proposed details changes. */
|
||||
entries(edit: any): { key: string; value: any }[] {
|
||||
if (!edit) return [];
|
||||
const fields: [string, any][] = [
|
||||
[
|
||||
'Linked Savings Account',
|
||||
edit.savingsAccountNo
|
||||
],
|
||||
[
|
||||
'Allow Dividends for Inactive Clients',
|
||||
edit.allowDividendCalculationForInactiveClients
|
||||
],
|
||||
[
|
||||
'Lockin Period Frequency',
|
||||
edit.lockinPeriodFrequency
|
||||
],
|
||||
[
|
||||
'Minimum Active Period Frequency',
|
||||
edit.minimumActivePeriodFrequency
|
||||
]
|
||||
];
|
||||
return fields
|
||||
.filter(
|
||||
([
|
||||
,
|
||||
value
|
||||
]) => value !== null && value !== undefined
|
||||
)
|
||||
.map(
|
||||
([
|
||||
key,
|
||||
value
|
||||
]) => ({ key, value })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the account's current details so the popup can show them next to what's proposed, then
|
||||
* opens the compare dialog -- mirrors PendingSavingsAccountEditsComponent#viewCompare.
|
||||
*/
|
||||
viewCompare(edit: any): void {
|
||||
this.selectedEdit = this.selectedEdit === edit ? null : edit;
|
||||
}
|
||||
|
||||
closeCompare(): void {
|
||||
this.selectedEdit = null;
|
||||
this.loadingCompareFor = edit;
|
||||
this.sharesService.getSharesAccountData(String(edit.shareAccountId), false).subscribe({
|
||||
next: (account: any) => {
|
||||
this.loadingCompareFor = null;
|
||||
this.dialog.open(ShareAccountEditCompareDialogComponent, {
|
||||
width: '640px',
|
||||
data: {
|
||||
accountNo: edit.accountNo,
|
||||
currentAccount: account,
|
||||
edit,
|
||||
rejectReason: edit.rejectReason
|
||||
}
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.loadingCompareFor = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
approve(edit: any): void {
|
||||
@@ -183,7 +267,6 @@ export class PendingShareAccountEditsComponent {
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.closeCompare();
|
||||
const url = this.router.url;
|
||||
this.router
|
||||
.navigateByUrl('/checker-inbox-and-tasks', { skipLocationChange: true })
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<h2 mat-dialog-title class="saecd-title">
|
||||
<mat-icon class="saecd-title__icon">difference</mat-icon>
|
||||
<span>
|
||||
Proposed changes — <strong>{{ accountNo }}</strong>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div mat-dialog-content class="saecd-content">
|
||||
@if (rows.length === 0) {
|
||||
<div class="saecd-empty">
|
||||
<mat-icon class="saecd-empty__icon">check_circle</mat-icon>
|
||||
<p class="saecd-empty__title">No changes to review</p>
|
||||
<p class="saecd-empty__text">This edit was submitted without changing any detail.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (rows.length > 0) {
|
||||
<table class="saecd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Current</th>
|
||||
<th>Proposed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of rows; track row.label) {
|
||||
<tr>
|
||||
<td class="saecd-table__label">{{ row.label }}</td>
|
||||
<td class="saecd-table__old">{{ row.current }}</td>
|
||||
<td class="saecd-table__new">{{ row.proposed }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@if (rejectReason) {
|
||||
<p class="saecd-reject-reason"><strong>Reject Reason:</strong> {{ rejectReason }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>{{ 'labels.buttons.Close' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
$border: #e2e8f0;
|
||||
$muted: #64748b;
|
||||
|
||||
.saecd-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
border-bottom: 1px solid $border;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&__icon {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.saecd-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid $border;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
color: $muted;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.4px;
|
||||
background: #f8fafc;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
width: 34%;
|
||||
}
|
||||
|
||||
&__old {
|
||||
color: #b91c1c;
|
||||
text-decoration: line-through;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
&__new {
|
||||
color: #15803d;
|
||||
font-weight: 600;
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
&__icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #16a34a;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: $muted;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
.saecd-reject-reason {
|
||||
margin: 20px 0 0;
|
||||
padding: 10px 12px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 6px;
|
||||
font-size: 12.5px;
|
||||
color: #991b1b;
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
interface DiffRow {
|
||||
label: string;
|
||||
current: string;
|
||||
proposed: string;
|
||||
}
|
||||
|
||||
/** SharePeriodFrequencyType: 0=Days, 1=Weeks, 2=Months, 3=Years. */
|
||||
const FREQUENCY_TYPE_LABELS: Record<number, string> = {
|
||||
0: 'Days',
|
||||
1: 'Weeks',
|
||||
2: 'Months',
|
||||
3: 'Years'
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows what a pending Share Account "details" edit actually changes, as a popup instead of an
|
||||
* inline panel -- mirrors SavingsAccountEditCompareDialogComponent's current-vs-proposed pattern.
|
||||
* A field the maker's edit form included in the payload (see EditShareAccountDetailsComponent#submit)
|
||||
* is still often equal to the account's current value -- e.g. allowDividendCalculationForInactiveClients
|
||||
* is always sent, and the maker may not have touched the Default Savings Account or Lock-in Period even
|
||||
* though those controls were part of the same form submit. Only rows whose displayed current and
|
||||
* proposed values actually differ are shown, so an unchanged field never appears as a false "change".
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-share-account-edit-compare-dialog',
|
||||
templateUrl: './share-account-edit-compare-dialog.component.html',
|
||||
styleUrls: ['./share-account-edit-compare-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatButton,
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class ShareAccountEditCompareDialogComponent implements OnInit {
|
||||
dialogRef = inject<MatDialogRef<ShareAccountEditCompareDialogComponent>>(MatDialogRef);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
accountNo = '';
|
||||
rows: DiffRow[] = [];
|
||||
rejectReason: string | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountNo = this.data.accountNo ?? '';
|
||||
this.rejectReason = this.data.rejectReason ?? null;
|
||||
|
||||
const current = this.data.currentAccount ?? {};
|
||||
const edit = this.data.edit ?? {};
|
||||
const candidates: { label: string; current: string; proposed: string; proposedRaw: any }[] = [];
|
||||
|
||||
candidates.push({
|
||||
label: 'Linked Savings Account',
|
||||
current: this.displayValue(current.savingsAccountNumber),
|
||||
proposed: this.displayValue(edit.savingsAccountNo),
|
||||
proposedRaw: edit.savingsAccountId
|
||||
});
|
||||
|
||||
candidates.push({
|
||||
label: 'Allow Dividends for Inactive Clients',
|
||||
current: this.displayValue(current.allowDividendCalculationForInactiveClients),
|
||||
proposed: this.displayValue(edit.allowDividendCalculationForInactiveClients),
|
||||
proposedRaw: edit.allowDividendCalculationForInactiveClients
|
||||
});
|
||||
|
||||
candidates.push({
|
||||
label: 'Minimum Active Period',
|
||||
current: this.displayPeriod(current.minimumActivePeriod, current.minimumActivePeriodTypeEnum?.id),
|
||||
proposed: this.displayPeriod(edit.minimumActivePeriodFrequency, edit.minimumActivePeriodFrequencyType),
|
||||
proposedRaw: edit.minimumActivePeriodFrequency
|
||||
});
|
||||
|
||||
candidates.push({
|
||||
label: 'Lock-in Period',
|
||||
current: this.displayPeriod(current.lockinPeriod, current.lockPeriodTypeEnum?.id),
|
||||
proposed: this.displayPeriod(edit.lockinPeriodFrequency, edit.lockinPeriodFrequencyType),
|
||||
proposedRaw: edit.lockinPeriodFrequency
|
||||
});
|
||||
|
||||
// A field the maker's edit form didn't touch was never sent (see the class doc), and a field
|
||||
// that was sent but landed back on the same value never actually changed anything -- both cases
|
||||
// are dropped so only genuine differences reach the popup.
|
||||
this.rows = candidates
|
||||
.filter((c) => c.proposedRaw !== null && c.proposedRaw !== undefined)
|
||||
.filter((c) => c.current !== c.proposed)
|
||||
.map(({ label, current: cur, proposed }) => ({ label, current: cur, proposed }));
|
||||
}
|
||||
|
||||
private displayPeriod(frequency: any, frequencyType: any): string {
|
||||
if (frequency === null || frequency === undefined) {
|
||||
return '—';
|
||||
}
|
||||
const typeLabel = FREQUENCY_TYPE_LABELS[frequencyType] ?? '';
|
||||
return typeLabel ? `${frequency} ${typeLabel}` : String(frequency);
|
||||
}
|
||||
|
||||
private displayValue(value: any): string {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '—';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
+43
-6
@@ -7,10 +7,41 @@
|
||||
-->
|
||||
|
||||
<div class="sar-container">
|
||||
<p class="sar-intro">
|
||||
Additional-share tranches waiting for approval, across every share account. Click a row for the full request — the
|
||||
board resolution, how it was funded, and the voucher or savings account behind it.
|
||||
</p>
|
||||
<!-- ── TITLE BAR ── -->
|
||||
<div class="sar-action-bar">
|
||||
<div class="sar-action-bar__left">
|
||||
<mat-icon class="sar-action-bar__icon">add_card</mat-icon>
|
||||
<div>
|
||||
<div class="sar-action-bar__title">Share Addition Requests</div>
|
||||
<div class="sar-action-bar__sub">
|
||||
Additional-share tranches waiting for approval, across every share account. Click a row for the full request —
|
||||
the board resolution, how it was funded, and the voucher or savings account behind it.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── FILTER BAR ── -->
|
||||
<div class="sar-filter-bar">
|
||||
<div class="sar-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by client, account or resolution no"
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="applyFilter()"
|
||||
/>
|
||||
@if (searchTerm) {
|
||||
<button type="button" class="sar-search-box__clear" (click)="searchTerm = ''; applyFilter()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="button" class="sar-btn sar-btn--refresh" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (errorMsg) {
|
||||
<div class="sar-error">
|
||||
@@ -27,7 +58,13 @@
|
||||
} @else if (requests.length === 0) {
|
||||
<div class="sar-empty">
|
||||
<mat-icon>info_outline</mat-icon>
|
||||
<span>No additional-share requests are waiting for approval.</span>
|
||||
<span>
|
||||
@if (allRequests.length > 0) {
|
||||
No requests match "{{ searchTerm }}".
|
||||
} @else {
|
||||
No additional-share requests are waiting for approval.
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="sar-table-wrap">
|
||||
@@ -53,7 +90,7 @@
|
||||
<td class="ta-right amount-cell">{{ req.amount | number: '1.2-2' }}</td>
|
||||
<td class="ta-left">{{ fundingLabel(req) }}</td>
|
||||
<td class="ta-left">{{ req.resolutionNumber || '—' }}</td>
|
||||
<td class="ta-left">{{ req.transactionDate }}</td>
|
||||
<td class="ta-left sar-dt">{{ req.transactionDate }}</td>
|
||||
<td class="ta-center sar-actions" (click)="$event.stopPropagation()">
|
||||
<button
|
||||
mat-stroked-button
|
||||
|
||||
+189
-29
@@ -6,32 +6,189 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$primary: #1a3a6b;
|
||||
$border: #d8e0ec;
|
||||
$muted: #6b7a90;
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
$muted: #64748b;
|
||||
$border: #e2e8f0;
|
||||
$danger: #b3261e;
|
||||
|
||||
.sar-container {
|
||||
padding: 8px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sar-intro {
|
||||
// ── Title bar — mirrors WithdrawalRequestsComponent's action bar ──────────
|
||||
.sar-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid $border;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 12px;
|
||||
color: $muted;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter bar ──────────────────────────────────────────────────────────
|
||||
.sar-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 20px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid $border;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sar-search-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
min-width: 260px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 0 0 3px rgb(15 79 60 / 8%);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 7px;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid $border;
|
||||
transition: background 0.15s;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
color: $muted;
|
||||
margin: 0 0 14px;
|
||||
background: #fff;
|
||||
color: #0d1b2a;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--refresh {
|
||||
padding: 0 8px;
|
||||
color: $muted;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sar-loading,
|
||||
.sar-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 48px 24px;
|
||||
font-size: 13px;
|
||||
color: $muted;
|
||||
background: #f4f7fb;
|
||||
border: 1px solid $border;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
|
||||
mat-icon {
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
.sar-error {
|
||||
@@ -44,53 +201,50 @@ $danger: #b3261e;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
margin: 16px 20px 0;
|
||||
}
|
||||
|
||||
.sar-table-wrap {
|
||||
overflow-x: auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.sar-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
border: 1px solid $border;
|
||||
border-radius: 6px;
|
||||
|
||||
th {
|
||||
background: #eef2f8;
|
||||
color: $primary;
|
||||
background: #f8fafc;
|
||||
color: $muted;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
padding: 9px 12px;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 10px 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #eceff4;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #1f2937;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: #f4f7fb;
|
||||
&:hover td {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ta-left {
|
||||
@@ -107,7 +261,13 @@ $danger: #b3261e;
|
||||
|
||||
.amount-cell {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
.sar-dt {
|
||||
font-size: 12px;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
.sar-actions {
|
||||
|
||||
+17
-1
@@ -8,6 +8,7 @@
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
@@ -36,6 +37,7 @@ import { ShareAdditionRequestDetailDialogComponent } from './share-addition-requ
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule
|
||||
@@ -49,8 +51,12 @@ export class ShareAdditionRequestsComponent implements OnInit {
|
||||
|
||||
loading = true;
|
||||
errorMsg = '';
|
||||
/** Raw, unfiltered list from the API -- search happens client-side over this since the endpoint
|
||||
* has no server-side filtering (it always returns every pending request in one shot). */
|
||||
allRequests: any[] = [];
|
||||
requests: any[] = [];
|
||||
actioningId: number | null = null;
|
||||
searchTerm = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
@@ -68,11 +74,21 @@ export class ShareAdditionRequestsComponent implements OnInit {
|
||||
})
|
||||
)
|
||||
.subscribe((res: any) => {
|
||||
this.requests = Array.isArray(res) ? res : [];
|
||||
this.allRequests = Array.isArray(res) ? res : [];
|
||||
this.applyFilter();
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(): void {
|
||||
const needle = this.searchTerm.trim().toLowerCase();
|
||||
this.requests = !needle
|
||||
? this.allRequests
|
||||
: this.allRequests.filter((req) =>
|
||||
`${req.clientName ?? ''} ${req.accountNo ?? ''} ${req.resolutionNumber ?? ''}`.toLowerCase().includes(needle)
|
||||
);
|
||||
}
|
||||
|
||||
/** How many receipts fund a cash tranche — the lead one plus any further instalments. */
|
||||
voucherCount(req: any): number {
|
||||
if (!req?.advanceCollectionId) {
|
||||
|
||||
+152
-61
@@ -6,69 +6,160 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Share Withdrawal Requests — Review, Approve or Reject</div>
|
||||
<div class="inv-form-grid" style="padding: 12px 16px">
|
||||
<label class="inv-form-field">
|
||||
<span>Status</span>
|
||||
<select [(ngModel)]="filterStatus" (change)="onFilterChange()">
|
||||
@for (opt of statusOptions; track opt.value) {
|
||||
<option [value]="opt.value">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field" style="align-self: end">
|
||||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="loading" (click)="load()">Refresh</button>
|
||||
</label>
|
||||
<div class="swr-tab">
|
||||
<!-- ── TITLE BAR ── -->
|
||||
<div class="swr-action-bar">
|
||||
<div class="swr-action-bar__left">
|
||||
<mat-icon class="swr-action-bar__icon">redeem</mat-icon>
|
||||
<div>
|
||||
<div class="swr-action-bar__title">Share Withdrawal Requests</div>
|
||||
<div class="swr-action-bar__sub">Review, approve or reject clerk share (redemption) withdrawal requests.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Client</th>
|
||||
<th class="num">Shares</th>
|
||||
<th>Resolution No.</th>
|
||||
<th>Meeting Date</th>
|
||||
<th>Clerk</th>
|
||||
<th>Manager</th>
|
||||
<th>Submitted At</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading) {
|
||||
<tr>
|
||||
<td colspan="10" class="pg-empty">Loading…</td>
|
||||
</tr>
|
||||
} @else if (loaded && requests.length === 0) {
|
||||
<tr>
|
||||
<td colspan="10" class="pg-empty">No {{ filterStatus | lowercase }} share withdrawal requests.</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (r of requests; track r.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ r.account_no }}</span>
|
||||
</td>
|
||||
<td>{{ r.client_name }}</td>
|
||||
<td class="num">{{ r.requested_shares }}</td>
|
||||
<td>{{ r.resolution_number }}</td>
|
||||
<td>{{ formatDateTime(r.meeting_date) }}</td>
|
||||
<td>{{ r.clerk_name }}</td>
|
||||
<td>{{ r.manager_name || r.manager_username || '—' }}</td>
|
||||
<td>{{ formatDateTime(r.submitted_at) }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="review(r)">Review</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
<!-- ── FILTER BAR ── -->
|
||||
<div class="swr-filter-bar">
|
||||
<div class="swr-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by account, client or resolution no"
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="onSearchChange()"
|
||||
/>
|
||||
@if (searchTerm) {
|
||||
<button type="button" class="swr-search-box__clear" (click)="searchTerm = ''; onSearchChange()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<span class="swr-fb-sep"></span>
|
||||
|
||||
<select class="swr-filter-select" [(ngModel)]="filterStatus" (change)="onFilterChange()">
|
||||
@for (opt of statusOptions; track opt.value) {
|
||||
<option [value]="opt.value">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<span class="swr-fb-sep"></span>
|
||||
|
||||
<div class="swr-presets">
|
||||
@for (p of datePresets; track p.key) {
|
||||
<button
|
||||
type="button"
|
||||
class="swr-preset-chip"
|
||||
[class.swr-preset-chip--active]="activePreset === p.key"
|
||||
(click)="applyPreset(p.key)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="swr-fb-sep"></span>
|
||||
|
||||
<div class="swr-date-range">
|
||||
<div class="swr-date-box">
|
||||
<span class="swr-date-box__lbl">From</span>
|
||||
<input
|
||||
class="swr-date-box__input"
|
||||
[matDatepicker]="fromPicker"
|
||||
[(ngModel)]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onFromDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="swr-date-box__btn" (click)="fromPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</div>
|
||||
<span class="swr-date-range__sep">—</span>
|
||||
<div class="swr-date-box">
|
||||
<span class="swr-date-box__lbl">To</span>
|
||||
<input
|
||||
class="swr-date-box__input"
|
||||
[matDatepicker]="toPicker"
|
||||
[(ngModel)]="toDate"
|
||||
[min]="fromDate"
|
||||
[max]="maxDate"
|
||||
(dateChange)="onToDateChange()"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="swr-date-box__btn" (click)="toPicker.open()">
|
||||
<mat-icon>event</mat-icon>
|
||||
</button>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="swr-btn swr-btn--refresh" (click)="load()" [disabled]="loading">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── TABLE ── -->
|
||||
@if (loading) {
|
||||
<div class="empty-state">
|
||||
<p class="empty-title">Loading…</p>
|
||||
</div>
|
||||
} @else if (loaded && requests.length === 0) {
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox empty-icon"></i>
|
||||
<p class="empty-title">No share withdrawal requests</p>
|
||||
<p class="empty-text">
|
||||
@if (filterStatus === 'PENDING') {
|
||||
There are no pending share withdrawal requests waiting for review.
|
||||
} @else {
|
||||
No {{ filterStatus | lowercase }} share withdrawal requests found.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="swr-table-wrap">
|
||||
<table class="swr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Account No</th>
|
||||
<th>Client</th>
|
||||
<th class="num">Shares</th>
|
||||
<th>Resolution No.</th>
|
||||
<th>Meeting Date</th>
|
||||
<th>Clerk</th>
|
||||
<th>Manager</th>
|
||||
<th>Submitted At</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (r of requests; track r.id) {
|
||||
<tr class="swr-row" (click)="review(r)">
|
||||
<td class="cell-mono">{{ r.account_no }}</td>
|
||||
<td class="swr-client">{{ r.client_name }}</td>
|
||||
<td class="num">{{ r.requested_shares }}</td>
|
||||
<td>{{ r.resolution_number }}</td>
|
||||
<td class="swr-dt">{{ formatDateTime(r.meeting_date) }}</td>
|
||||
<td>{{ r.clerk_name }}</td>
|
||||
<td>{{ r.manager_name || r.manager_username || '—' }}</td>
|
||||
<td class="swr-dt">{{ formatDateTime(r.submitted_at) }}</td>
|
||||
<td>
|
||||
<span class="swr-status" [ngClass]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
||||
</td>
|
||||
<td (click)="$event.stopPropagation()">
|
||||
<button type="button" class="swr-btn swr-btn--review" (click)="review(r)">
|
||||
<mat-icon>rate_review</mat-icon>
|
||||
Review
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showReviewModal && reviewRequestId) {
|
||||
|
||||
+424
@@ -6,6 +6,430 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.swr-tab {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// ── Title bar — mirrors WithdrawalRequestsComponent's action bar ──────────
|
||||
.swr-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0d1b2a;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter bar ──────────────────────────────────────────────────────────
|
||||
.swr-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 20px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.swr-fb-sep {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
background: #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.swr-search-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
min-width: 260px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 0 0 3px rgb(15 79 60 / 8%);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swr-filter-select {
|
||||
height: 34px;
|
||||
min-width: 130px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #0f4f3c;
|
||||
}
|
||||
}
|
||||
|
||||
.swr-presets {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.swr-preset-chip {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: #0f4f3c;
|
||||
color: #0f4f3c;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #0f4f3c;
|
||||
border-color: #0f4f3c;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #0a3d2e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swr-date-range {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&__sep {
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
padding: 0 2px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.swr-date-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 34px;
|
||||
padding: 0 6px 0 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
}
|
||||
|
||||
&__lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
&__input {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
width: 76px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&::placeholder {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #0f4f3c;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swr-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 7px;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: background 0.15s;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
color: #0d1b2a;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--refresh {
|
||||
padding: 0 8px;
|
||||
color: #64748b;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
&--review {
|
||||
background: #0f4f3c;
|
||||
color: #fff;
|
||||
border-color: #0f4f3c;
|
||||
|
||||
&:hover {
|
||||
background: #0a3d2e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swr-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.swr-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #f8fafc;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.swr-row {
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover td {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.swr-client {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.swr-dt {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.swr-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
|
||||
&--pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
&--approved {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
&--rejected {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: #94a3b8;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 40px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Review popup (unchanged) ────────────────────────────────────────────
|
||||
|
||||
@keyframes swr-list-popup-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
+82
-5
@@ -9,11 +9,15 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { ClerkShareWithdrawalService } from '../../../cashier/clerk-share-withdrawal.service';
|
||||
import { ShareWithdrawalRequestComponent } from '../../../clerk/share-withdrawal-request/share-withdrawal-request.component';
|
||||
import { SettingsService } from '../../../settings/settings.service';
|
||||
|
||||
/** Approval Center — Share Withdrawal Requests tab. A Bank Manager (checker) reviews, approves
|
||||
* or rejects share withdrawal requests submitted from the Clerk Dashboard. Mirrors
|
||||
@@ -32,11 +36,15 @@ import { ShareWithdrawalRequestComponent } from '../../../clerk/share-withdrawal
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatIconModule,
|
||||
MatNativeDateModule,
|
||||
MatDatepickerModule,
|
||||
ShareWithdrawalRequestComponent
|
||||
]
|
||||
})
|
||||
export class ShareWithdrawalRequestsComponent implements OnInit {
|
||||
private service = inject(ClerkShareWithdrawalService);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
requests: any[] = [];
|
||||
totalRecords = 0;
|
||||
@@ -44,10 +52,22 @@ export class ShareWithdrawalRequestsComponent implements OnInit {
|
||||
loaded = false;
|
||||
|
||||
filterStatus = 'PENDING';
|
||||
searchTerm = '';
|
||||
fromDate: Date | null = null;
|
||||
toDate: Date | null = null;
|
||||
maxDate!: Date;
|
||||
activePreset = 'all';
|
||||
|
||||
showReviewModal = false;
|
||||
reviewRequestId: number | null = null;
|
||||
|
||||
readonly datePresets = [
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'week', label: 'This Week' },
|
||||
{ key: 'month', label: 'This Month' },
|
||||
{ key: 'all', label: 'All' }
|
||||
];
|
||||
|
||||
get statusOptions(): { value: string; label: string }[] {
|
||||
return [
|
||||
{ value: 'PENDING', label: 'Pending' },
|
||||
@@ -57,13 +77,23 @@ export class ShareWithdrawalRequestsComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const today = new Date(this.settingsService.businessDate);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
this.maxDate = today;
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.service
|
||||
.getPendingRequests(this.filterStatus, 0, 50)
|
||||
.getPendingRequests(
|
||||
this.filterStatus,
|
||||
0,
|
||||
50,
|
||||
this.isoDate(this.fromDate),
|
||||
this.isoDate(this.toDate),
|
||||
this.searchTerm.trim() || undefined
|
||||
)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
.subscribe((res: any) => {
|
||||
this.requests = res?.pageItems ?? [];
|
||||
@@ -77,6 +107,53 @@ export class ShareWithdrawalRequestsComponent implements OnInit {
|
||||
this.load();
|
||||
}
|
||||
|
||||
onSearchChange(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
applyPreset(key: string): void {
|
||||
this.activePreset = key;
|
||||
const today = new Date(this.settingsService.businessDate);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (key === 'today') {
|
||||
this.fromDate = new Date(today);
|
||||
this.toDate = new Date(today);
|
||||
} else if (key === 'week') {
|
||||
const start = new Date(today);
|
||||
start.setDate(today.getDate() - today.getDay());
|
||||
this.fromDate = start;
|
||||
this.toDate = new Date(today);
|
||||
} else if (key === 'month') {
|
||||
this.fromDate = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
this.toDate = new Date(today);
|
||||
} else {
|
||||
this.fromDate = null;
|
||||
this.toDate = null;
|
||||
}
|
||||
this.load();
|
||||
}
|
||||
|
||||
onFromDateChange(): void {
|
||||
this.activePreset = '';
|
||||
if (this.fromDate && this.toDate && this.fromDate > this.toDate) {
|
||||
this.toDate = new Date(this.fromDate);
|
||||
}
|
||||
this.load();
|
||||
}
|
||||
|
||||
onToDateChange(): void {
|
||||
this.activePreset = '';
|
||||
this.load();
|
||||
}
|
||||
|
||||
private isoDate(d: Date | null): string | undefined {
|
||||
if (!d) return undefined;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
review(req: any): void {
|
||||
this.reviewRequestId = req.id;
|
||||
this.showReviewModal = true;
|
||||
@@ -94,11 +171,11 @@ export class ShareWithdrawalRequestsComponent implements OnInit {
|
||||
|
||||
statusClass(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
PENDING: 'inv-badge--active',
|
||||
APPROVED: 'inv-badge--matured',
|
||||
REJECTED: 'inv-badge--written_off'
|
||||
PENDING: 'swr-status--pending',
|
||||
APPROVED: 'swr-status--approved',
|
||||
REJECTED: 'swr-status--rejected'
|
||||
};
|
||||
return `inv-badge ${map[status] ?? ''}`;
|
||||
return map[status] ?? '';
|
||||
}
|
||||
|
||||
statusLabel(status: string): string {
|
||||
|
||||
+17
@@ -20,6 +20,23 @@
|
||||
|
||||
<!-- ── FILTER BAR ── -->
|
||||
<div class="wri-filter-bar">
|
||||
<div class="wri-search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by account no, client, voucher or token"
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="onSearchChange()"
|
||||
/>
|
||||
@if (searchTerm) {
|
||||
<button type="button" class="wri-search-box__clear" (click)="searchTerm = ''; onSearchChange()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<span class="wri-fb-sep"></span>
|
||||
|
||||
<select class="wri-filter-select" [(ngModel)]="filterStatus" (change)="onFilterChange()">
|
||||
@for (opt of statusOptions; track opt.value) {
|
||||
<option [value]="opt.value">{{ opt.label }}</option>
|
||||
|
||||
+71
@@ -65,6 +65,77 @@
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.wri-search-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
min-width: 260px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 17px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #0f4f3c;
|
||||
box-shadow: 0 0 0 3px rgb(15 79 60 / 8%);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
color: #0d1b2a;
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wri-filter-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+14
-1
@@ -64,6 +64,8 @@ export class WithdrawalRequestsComponent implements OnInit {
|
||||
activePreset: string = 'today';
|
||||
loading = false;
|
||||
loaded = false;
|
||||
searchTerm = '';
|
||||
private searchDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
readonly datePresets = [
|
||||
{ key: 'today', label: 'Today' },
|
||||
@@ -96,7 +98,8 @@ export class WithdrawalRequestsComponent implements OnInit {
|
||||
this.pageIndex * this.pageSize,
|
||||
this.pageSize,
|
||||
this.isoDate(this.fromDate),
|
||||
this.isoDate(this.toDate)
|
||||
this.isoDate(this.toDate),
|
||||
this.searchTerm.trim() || undefined
|
||||
)
|
||||
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
|
||||
.subscribe((res: any) => {
|
||||
@@ -107,6 +110,16 @@ export class WithdrawalRequestsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
onSearchChange(): void {
|
||||
if (this.searchDebounce) {
|
||||
clearTimeout(this.searchDebounce);
|
||||
}
|
||||
this.searchDebounce = setTimeout(() => {
|
||||
this.pageIndex = 0;
|
||||
this.load();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.pageSize = event.pageSize;
|
||||
this.pageIndex = event.pageIndex;
|
||||
|
||||
Reference in New Issue
Block a user