feat(approvals): live pending-count badges on every Approval Center tab and the Approvals menu

The badges are pushed, not polled. ApprovalCountsService opens one
Server-Sent Events stream (/approvals/counts/stream) through HttpClient so the
login and tenant headers apply; the server writes a `counts` event the moment
the numbers change.

- Total badge on the Approvals menu, count badge on 26 Approval Center tabs
  (Reject Account and Terminated Account are lists, not requests: no badge).
- Last numbers are kept per login in localStorage, so a page refresh shows them
  at once until the stream's first event makes them exact.
- Stream recycles every few minutes and reconnects at once; after a failure it
  retries with a growing wait and reads /approvals/counts meanwhile.
- NO_PROGRESS_BAR token: the background stream does not run the top progress bar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:49:36 +05:30
co-authored by Claude Sonnet 5.5
parent ab8ed7e67e
commit 4d205672df
9 changed files with 368 additions and 3 deletions
@@ -31,6 +31,9 @@
>
<fa-icon icon="inbox" class="ac-tab__ic"></fa-icon>
<span>Checker Inbox</span>
@if (count('checker-inbox')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('checker-inbox') }}</span>
}
</a>
<a
class="ac-tab"
@@ -40,10 +43,16 @@
>
<fa-icon icon="user-check" class="ac-tab__ic"></fa-icon>
<span>Client Approval</span>
@if (count('client-approval')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('client-approval') }}</span>
}
</a>
<a class="ac-tab" routerLink="./loan-approval" routerLinkActive="ac-tab--active" *mifosxHasPermission="'READ_LOAN'">
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Loan Approval</span>
@if (count('loan-approval')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('loan-approval') }}</span>
}
</a>
<a
class="ac-tab"
@@ -53,6 +62,9 @@
>
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Loan Disbursal</span>
@if (count('loan-disbursal')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('loan-disbursal') }}</span>
}
</a>
<a
class="ac-tab"
@@ -62,6 +74,9 @@
>
<fa-icon icon="calendar-alt" class="ac-tab__ic"></fa-icon>
<span>Reschedule Loan</span>
@if (count('reschedule-loan')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('reschedule-loan') }}</span>
}
</a>
<a
class="ac-tab"
@@ -71,6 +86,9 @@
>
<fa-icon icon="hourglass-half" class="ac-tab__ic"></fa-icon>
<span>Pending Account</span>
@if (count('pending-account')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('pending-account') }}</span>
}
</a>
<a
class="ac-tab"
@@ -80,6 +98,9 @@
>
<fa-icon icon="hourglass-half" class="ac-tab__ic"></fa-icon>
<span>Pending Share Accounts</span>
@if (count('pending-share-accounts')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('pending-share-accounts') }}</span>
}
</a>
<a
class="ac-tab"
@@ -89,6 +110,9 @@
>
<fa-icon icon="piggy-bank" class="ac-tab__ic"></fa-icon>
<span>Product Approval</span>
@if (count('product-approval')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('product-approval') }}</span>
}
</a>
<a
class="ac-tab"
@@ -98,6 +122,9 @@
>
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Account Edits</span>
@if (count('pending-savings-account-edits')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('pending-savings-account-edits') }}</span>
}
</a>
<a
class="ac-tab"
@@ -107,6 +134,9 @@
>
<fa-icon icon="edit" class="ac-tab__ic"></fa-icon>
<span>Pending Client Edits</span>
@if (count('pending-client-edits')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('pending-client-edits') }}</span>
}
</a>
<a
class="ac-tab"
@@ -116,6 +146,9 @@
>
<fa-icon icon="user-cog" class="ac-tab__ic"></fa-icon>
<span>Pending User Approvals</span>
@if (count('pending-user-edits')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('pending-user-edits') }}</span>
}
</a>
<a
class="ac-tab"
@@ -143,6 +176,9 @@
>
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Withdrawal Requests</span>
@if (count('withdrawal-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('withdrawal-requests') }}</span>
}
</a>
<a
class="ac-tab"
@@ -152,6 +188,9 @@
>
<fa-icon icon="exchange" class="ac-tab__ic"></fa-icon>
<span>Cash Transfers</span>
@if (count('cash-transfers')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('cash-transfers') }}</span>
}
</a>
<a
class="ac-tab"
@@ -161,6 +200,9 @@
>
<fa-icon icon="undo" class="ac-tab__ic"></fa-icon>
<span>Voucher Corrections</span>
@if (count('voucher-corrections')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('voucher-corrections') }}</span>
}
</a>
<a
class="ac-tab"
@@ -170,6 +212,9 @@
>
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Share Withdrawal Requests</span>
@if (count('share-withdrawal-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('share-withdrawal-requests') }}</span>
}
</a>
<a
class="ac-tab"
@@ -179,6 +224,9 @@
>
<fa-icon icon="coins" class="ac-tab__ic"></fa-icon>
<span>Dividend Requests</span>
@if (count('dividend-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('dividend-requests') }}</span>
}
</a>
<a
class="ac-tab"
@@ -188,6 +236,9 @@
>
<fa-icon icon="money-check-alt" class="ac-tab__ic"></fa-icon>
<span>Share Addition Requests</span>
@if (count('share-addition-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('share-addition-requests') }}</span>
}
</a>
<a
class="ac-tab"
@@ -197,6 +248,9 @@
>
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Deposit Requests</span>
@if (count('deposit-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('deposit-requests') }}</span>
}
</a>
<a
class="ac-tab"
@@ -206,10 +260,16 @@
>
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Disbursement & Transfers</span>
@if (count('disbursement-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('disbursement-requests') }}</span>
}
</a>
<a class="ac-tab" routerLink="./dds-approvals" routerLinkActive="ac-tab--active">
<fa-icon icon="coins" class="ac-tab__ic"></fa-icon>
<span>DDS Approvals</span>
@if (count('dds-approvals')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('dds-approvals') }}</span>
}
</a>
<a
class="ac-tab"
@@ -219,6 +279,9 @@
>
<fa-icon icon="money-check" class="ac-tab__ic"></fa-icon>
<span>Cheque Book Requests</span>
@if (count('chequebook-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('chequebook-requests') }}</span>
}
</a>
<a
class="ac-tab"
@@ -228,26 +291,41 @@
>
<fa-icon icon="lock" class="ac-tab__ic"></fa-icon>
<span>Closure Requests</span>
@if (count('closure-requests')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('closure-requests') }}</span>
}
</a>
<a class="ac-tab" routerLink="./dd-issue-approvals" routerLinkActive="ac-tab--active">
<fa-icon icon="receipt" class="ac-tab__ic"></fa-icon>
<span>DD Approvals</span>
@if (count('dd-issue-approvals')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('dd-issue-approvals') }}</span>
}
</a>
<a class="ac-tab" routerLink="./dd-reprint-approvals" routerLinkActive="ac-tab--active">
<fa-icon icon="print" class="ac-tab__ic"></fa-icon>
<span>DD Reprint Requests</span>
@if (count('dd-reprint-approvals')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('dd-reprint-approvals') }}</span>
}
</a>
<!-- Beside the reprint queue because the two arrive together from the counter, but they are
different decisions: a reprint re-prints the same leaflet, a revoke spends a fresh one. -->
<a class="ac-tab" routerLink="./dd-revoke-approvals" routerLinkActive="ac-tab--active">
<fa-icon icon="ban" class="ac-tab__ic"></fa-icon>
<span>DD Revoke Requests</span>
@if (count('dd-revoke-approvals')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('dd-revoke-approvals') }}</span>
}
</a>
<!-- Raised from the Risk Review Report. Approving moves the customer's KYC rating and their
re-KYC due date together, so it belongs with the other maker-checker queues. -->
<a class="ac-tab" routerLink="./risk-change-approvals" routerLinkActive="ac-tab--active">
<fa-icon icon="shield-alt" class="ac-tab__ic"></fa-icon>
<span>Risk Category Changes</span>
@if (count('risk-change-approvals')) {
<span class="ac-tab__count" title="Waiting for action">{{ count('risk-change-approvals') }}</span>
}
</a>
</nav>