From 9739ff0758e94df751a787c063d70642231a44a2 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 1 Sep 2026 07:43:07 +0530 Subject: [PATCH] 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. --- .../cashier/clerk-share-withdrawal.service.ts | 10 +- src/app/cashier/clerk-withdrawal.service.ts | 10 +- .../general-tab/general-tab.component.html | 6 + .../shares-account-terms-step.component.ts | 32 +- .../shares-account-view.component.html | 10 +- src/app/shares/shares-routing.module.ts | 9 + .../dividend-requests.component.html | 148 +++--- .../dividend-requests.component.scss | 250 ++++++++++- .../dividend-requests.component.ts | 18 +- ...nding-savings-account-edits.component.html | 231 +++++++--- ...nding-savings-account-edits.component.scss | 328 +++++++++++++- ...pending-savings-account-edits.component.ts | 162 +++++-- ...pending-share-account-edits.component.html | 229 ++++++---- ...pending-share-account-edits.component.scss | 401 +++++++++++++---- .../pending-share-account-edits.component.ts | 183 +++++--- ...account-edit-compare-dialog.component.html | 52 +++ ...account-edit-compare-dialog.component.scss | 119 +++++ ...e-account-edit-compare-dialog.component.ts | 129 ++++++ .../share-addition-requests.component.html | 49 +- .../share-addition-requests.component.scss | 218 +++++++-- .../share-addition-requests.component.ts | 18 +- .../share-withdrawal-requests.component.html | 213 ++++++--- .../share-withdrawal-requests.component.scss | 424 ++++++++++++++++++ .../share-withdrawal-requests.component.ts | 87 +++- .../withdrawal-requests.component.html | 17 + .../withdrawal-requests.component.scss | 71 +++ .../withdrawal-requests.component.ts | 15 +- 27 files changed, 2932 insertions(+), 507 deletions(-) create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.ts diff --git a/src/app/cashier/clerk-share-withdrawal.service.ts b/src/app/cashier/clerk-share-withdrawal.service.ts index fde90ad02..ff93d4294 100644 --- a/src/app/cashier/clerk-share-withdrawal.service.ts +++ b/src/app/cashier/clerk-share-withdrawal.service.ts @@ -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 { + getPendingRequests( + status = 'PENDING', + offset = 0, + limit = 20, + fromDate?: string, + toDate?: string, + search?: string + ): Observable { 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 }); } diff --git a/src/app/cashier/clerk-withdrawal.service.ts b/src/app/cashier/clerk-withdrawal.service.ts index c137da4b4..391b4f5d7 100644 --- a/src/app/cashier/clerk-withdrawal.service.ts +++ b/src/app/cashier/clerk-withdrawal.service.ts @@ -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 { + getPendingRequests( + status = 'PENDING', + offset = 0, + limit = 20, + fromDate?: string, + toDate?: string, + search?: string + ): Observable { 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 }); } diff --git a/src/app/savings/savings-account-view/general-tab/general-tab.component.html b/src/app/savings/savings-account-view/general-tab/general-tab.component.html index 68ddc9f50..29831808d 100644 --- a/src/app/savings/savings-account-view/general-tab/general-tab.component.html +++ b/src/app/savings/savings-account-view/general-tab/general-tab.component.html @@ -35,6 +35,12 @@ {{ savingsAccountData.clientName || savingsAccountData.groupName || '—' }} + @if (savingsAccountData.shareLinkingEnabled) { +
+ {{ 'labels.inputs.Linked Share Account' | translate }} + {{ savingsAccountData.shareAccountNo || 'Unassigned' }} +
+ } @if (!savingsAccountData.status.rejected && !savingsAccountData.status.submittedAndPendingApproval) {
{{ 'labels.inputs.Activated On' | translate }} diff --git a/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts b/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts index 69215fdd1..e18dbd2d0 100644 --- a/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts +++ b/src/app/shares/shares-account-stepper/shares-account-terms-step/shares-account-terms-step.component.ts @@ -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(); diff --git a/src/app/shares/shares-account-view/shares-account-view.component.html b/src/app/shares/shares-account-view/shares-account-view.component.html index 816b8bd5d..6d1787fc9 100644 --- a/src/app/shares/shares-account-view/shares-account-view.component.html +++ b/src/app/shares/shares-account-view/shares-account-view.component.html @@ -105,15 +105,7 @@ @for (option of buttonConfig.options; track option) { - diff --git a/src/app/shares/shares-routing.module.ts b/src/app/shares/shares-routing.module.ts index 83cb8e0fc..a2f7f614f 100644 --- a/src/app/shares/shares-routing.module.ts +++ b/src/app/shares/shares-routing.module.ts @@ -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' }, diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.html index c4f7c1c2a..3b8c5f29f 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.html @@ -6,63 +6,101 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- -
- +
+ +
+
+ payments +
+
Dividend Requests
+
Every Share Product dividend declaration still awaiting approval.
+
+
- - - - - - - - - - - - - @if (loading) { - - - - } @else if (loaded && rows.length === 0) { - - - - } @else { - - @for (row of rows; track row.dividendId) { - - - - - - - - - } + + +
+
-
Share ProductDividend PeriodTotal AmountMembersDeclared On
Loading…
No dividend declarations are waiting for approval.
{{ row.productName }}{{ row.dividendPeriodStartDate }} → {{ row.dividendPeriodEndDate }}{{ row.amount | number: '1.2-2' }}{{ row.memberCount }}{{ row.declaredOn | slice: 0 : 10 }} - - - @if (approveError[row.dividendId]) { -
{{ approveError[row.dividendId] }}
- } -
+
+ + +
+ + + @if (loading) { +
+

Loading…

+
+ } @else if (loaded && rows.length === 0) { +
+ check_circle +

No dividend declarations awaiting approval

+

+ @if (allRows.length > 0) { + No declarations match "{{ searchTerm }}". + } @else { + Every declared dividend has already been actioned. + } +

+
+ } @else { +
+ + + + + + + + + + + + + + @for (row of rows; track row.dividendId) { + + + + + + + + + } + +
Share ProductDividend PeriodTotal AmountMembersDeclared On
{{ row.productName }}{{ row.dividendPeriodStartDate }} → {{ row.dividendPeriodEndDate }}{{ row.amount | number: '1.2-2' }}{{ row.memberCount }}{{ row.declaredOn | slice: 0 : 10 }} + + + @if (approveError[row.dividendId]) { +
{{ approveError[row.dividendId] }}
+ } +
+
+ }
@if (showViewModal && viewProductId && viewDividendId) { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.scss index c8b7cf2a7..0f370404d 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.scss @@ -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; - } -} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.ts index 55fdfe891..896aa456e 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/dividend-requests/dividend-requests.component.ts @@ -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 = {}; @@ -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]; diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html index e11ff7345..913786046 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.html @@ -7,88 +7,175 @@ -->
+ +
+
+ edit_note +
+
Pending Account Edits
+
Review, approve or reject proposed Nominee / Digital-Feature edits.
+
+
+
+ @if (showData) { -
- - search - - + +
+ + + + +
+ @for (p of datePresets; track p.key) { + + } +
+ + + +
+
+ From + + + +
+ — +
+ To + + + +
+
+ +
-
- - - - - + @if (dataSource.data.length === 0) { +
+ +

No matching requests

+

No pending edits match the current search or date range.

+
+ } @else { +
+
Account No.{{ edit.accountNo }}
+ + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - -
Account No.{{ edit.accountNo }}Account Type - - {{ edit.accountType }} - - Account Type + + {{ edit.accountType }} + + Submitted By{{ edit.makerUsername }}Submitted By{{ edit.makerUsername }}Submitted On{{ formatDate(edit.createdOn) }}Submitted On{{ formatDate(edit.createdOn) }}Hold Amount - @if (edit.holdAmount) { - {{ edit.holdAmount }} - } @else { - — - } - Hold Amount + @if (edit.holdAmount) { + {{ edit.holdAmount }} + } @else { + — + } + Status - {{ edit.status }} - Status + {{ edit.status }} + Actions - - - - Actions + + + +
-
+ + + +
+ } } @if (!showData) { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss index 8645ecba1..0481558b4 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.scss @@ -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; diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts index e1f307f0c..422632f21 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-savings-account-edits/pending-savings-account-edits.component.ts @@ -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 { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html index 226cb7e37..98510b4f2 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.html @@ -7,94 +7,153 @@ -->
+ +
+
+ edit_note +
+
Pending Share Account Edits
+
Review, approve or reject proposed Share Account detail edits.
+
+
+
+ @if (showData) { -
- - search - - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Account No.{{ edit.accountNo }}Client{{ edit.clientName }}Submitted By{{ edit.makerUsername }}Submitted On{{ formatDate(edit.createdOn) }}Status - {{ edit.status }} - Actions - - - -
-
- - @if (selectedEdit) { -
-
-

Proposed Changes — Account "{{ selectedEdit.accountNo }}"

- -
- -
-

Details

- - - - - - - - - @for (row of entries(selectedEdit); track row.key) { - - - - - } - -
FieldProposed Value
{{ row.key }}{{ row.value }}
-
- - @if (selectedEdit.rejectReason) { -

Reject Reason: {{ selectedEdit.rejectReason }}

+ +
+ + + + +
+ @for (p of datePresets; track p.key) { + + } +
+ + + +
+
+ From + + + +
+ — +
+ To + + + +
+
+ + +
+ + @if (dataSource.data.length === 0) { +
+ +

No matching requests

+

No pending edits match the current search or date range.

+
+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account No.{{ edit.accountNo }}Client{{ edit.clientName }}Submitted By{{ edit.makerUsername }}Submitted On{{ formatDate(edit.createdOn) }}Status + {{ edit.status }} + Actions + + + +
+
} } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss index 2b605f603..c591c5b9b 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.scss @@ -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; diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts index 730c62163..f29fd2345 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/pending-share-account-edits.component.ts @@ -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 }) diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.html new file mode 100644 index 000000000..50356e553 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.html @@ -0,0 +1,52 @@ + +

+ difference + + Proposed changes — {{ accountNo }} + +

+ +
+ @if (rows.length === 0) { +
+ check_circle +

No changes to review

+

This edit was submitted without changing any detail.

+
+ } + + @if (rows.length > 0) { + + + + + + + + + + @for (row of rows; track row.label) { + + + + + + } + +
FieldCurrentProposed
{{ row.label }}{{ row.current }}{{ row.proposed }}
+ } + + @if (rejectReason) { +

Reject Reason: {{ rejectReason }}

+ } +
+ + + + diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.scss new file mode 100644 index 000000000..e9a93648f --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.scss @@ -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; +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.ts new file mode 100644 index 000000000..b26241c59 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/pending-share-account-edits/share-account-edit-compare-dialog/share-account-edit-compare-dialog.component.ts @@ -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 = { + 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); + 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); + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-requests.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-requests.component.html index ecd6fa5e5..df9eb181e 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-requests.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/share-addition-requests/share-addition-requests.component.html @@ -7,10 +7,41 @@ -->
-

- 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. -

+ +
+
+ add_card +
+
Share Addition Requests
+
+ 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. +
+
+
+
+ + +
+ + + +
@if (errorMsg) {
@@ -27,7 +58,13 @@ } @else if (requests.length === 0) {
info_outline - No additional-share requests are waiting for approval. + + @if (allRequests.length > 0) { + No requests match "{{ searchTerm }}". + } @else { + No additional-share requests are waiting for approval. + } +
} @else {
@@ -53,7 +90,7 @@ {{ req.amount | number: '1.2-2' }} {{ fundingLabel(req) }} {{ req.resolutionNumber || '—' }} - {{ req.transactionDate }} + {{ req.transactionDate }} - +
+ +
+
+ redeem +
+
Share Withdrawal Requests
+
Review, approve or reject clerk share (redemption) withdrawal requests.
+
+
- - - - - - - - - - - - - - - - - @if (loading) { - - - - } @else if (loaded && requests.length === 0) { - - - - } @else { - @for (r of requests; track r.id) { - - - - - - - - - - - - - } + + +
+
-
Account NoClientSharesResolution No.Meeting DateClerkManagerSubmitted AtStatus
Loading…
No {{ filterStatus | lowercase }} share withdrawal requests.
- {{ r.account_no }} - {{ r.client_name }}{{ r.requested_shares }}{{ r.resolution_number }}{{ formatDateTime(r.meeting_date) }}{{ r.clerk_name }}{{ r.manager_name || r.manager_username || '—' }}{{ formatDateTime(r.submitted_at) }} - {{ statusLabel(r.status) }} - - -
+
+ + + + + + + +
+ @for (p of datePresets; track p.key) { + + } +
+ + + +
+
+ From + + + +
+ — +
+ To + + + +
+
+ + +
+ + + @if (loading) { +
+

Loading…

+
+ } @else if (loaded && requests.length === 0) { +
+ +

No share withdrawal requests

+

+ @if (filterStatus === 'PENDING') { + There are no pending share withdrawal requests waiting for review. + } @else { + No {{ filterStatus | lowercase }} share withdrawal requests found. + } +

+
+ } @else { +
+ + + + + + + + + + + + + + + + + @for (r of requests; track r.id) { + + + + + + + + + + + + + } + +
Account NoClientSharesResolution No.Meeting DateClerkManagerSubmitted AtStatus
{{ r.account_no }}{{ r.client_name }}{{ r.requested_shares }}{{ r.resolution_number }}{{ formatDateTime(r.meeting_date) }}{{ r.clerk_name }}{{ r.manager_name || r.manager_username || '—' }}{{ formatDateTime(r.submitted_at) }} + {{ statusLabel(r.status) }} + + +
+
+ }
@if (showReviewModal && reviewRequestId) { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.scss index e3b249c01..0b1d83d37 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.scss @@ -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; diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.ts index 3c21d21ad..89f031056 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.ts +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/share-withdrawal-requests/share-withdrawal-requests.component.ts @@ -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 = { - 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 { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.component.html index a74ed81e5..aca962b2e 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/withdrawal-requests/withdrawal-requests.component.html @@ -20,6 +20,23 @@
+ + + +