@if (activeSection === 'alerts') {
KYC Alert Dashboard
@if (alertDateOpen) {
{{ alertFilter ? alertLabels[alertFilter] : 'Accounts' }} — {{ alertDateHeading }}: {{ alertDateOpen | date: 'dd MMM yyyy' }}
{{ alertDateRows.length }} account(s) — tick the ones to notify together, or use Notify on a row
{{ bulkTargets.length }} customer(s) selected
@if (!alertFilter) { } @for (r of pagedDayRows; track $index) { @if (!alertFilter) { } } @empty { }
Customer / account ContactState{{ alertDateHeading }} Balance Next stage Last notice sent Next to send Action
{{ r.name }}
{{ r.account }} · {{ r.branch }}
@if (contactOf(r.clientId); as ni) { @if (ni.hasMobile || ni.hasEmail) {
@if (ni.hasMobile) { Mobile } @if (ni.hasEmail) { Email }
} @else { No mobile / email
Reach by call or visit
} } @else { … }
{{ r.stage }}
{{ r.since | date: 'dd MMM yyyy' }}
{{ r.days }} {{ r.daysLabel }}
{{ (r.balance ?? alertNoticeInfo[r.clientId]?.balance) == null ? '—' : '₹' + (r.balance ?? alertNoticeInfo[r.clientId]?.balance | number: '1.2-2') }}
{{ r.next }}
@if (r.nextDate) {
{{ r.nextDate | date: 'dd MMM yyyy' }}
}
{{ lastNoticeText(r.clientId) }}
@if (alertNoticeInfo[r.clientId]?.count) {
{{ alertNoticeInfo[r.clientId].count }} notice(s) on record
@for (c of doneChannelsFor(r.clientId, r.stageKey); track c) { ✓ {{ auditChannelLabels[c] || c }} }
@if (earlierStages(r.clientId, r.stageKey).length) {
Earlier notices: {{ earlierStages(r.clientId, r.stageKey).join(', ') }}
} }
{{ nextNoticeStep(r) }}
@if (queuedChannels(r.clientId).length) {
Waiting in queue: {{ queuedChannels(r.clientId).join(', ') }}
} @if (nextAutoReminder(r); as auto) {
Automatic: {{ auto.date | date: 'dd MMM yyyy' }} — {{ auto.label }}
}
@if (hasNoContact(r.clientId)) { } @if (r.stageKey !== 'ESCHEAT') {
}
{{ statusLoading ? 'Loading…' : 'No account matches these filters.' }}
@if (alertDateRows.length > 0) {
{{ dayEffPage * daySize + 1 }}–{{ min(alertDateRows.length, (dayEffPage + 1) * daySize) }} of {{ alertDateRows.length }} accounts Rows per page Page {{ dayEffPage + 1 }} of {{ dayCount() }}
}
}
{{ expiringRows.length }}
Expiring in 30 days
Re-KYC falls due soon
{{ expiryTotals.grace }}
Overdue — Grace Period
KYC lapsed, still operative
{{ expiryTotals.partialFreeze }}
Debit Freeze
Debits blocked
{{ expiryTotals.inoperative }}
Inoperative
KYC lapsed {{ expiryPolicy.inoperativeDays }}d+
{{ dormancyTotals.inactive }}
Inactive
No customer txn {{ dormancyPolicy.inactiveDays }}d
{{ dormancyTotals.dormant }}
Dormant
No customer txn {{ dormancyPolicy.dormantDays }}d
{{ dormancyTotals.escheat }}
Escheat (DEAF)
RBI DEAF remittance
@if (alertFiltersActive) {
{{ alertFilter ? alertLabels[alertFilter] : 'Matching accounts' }}
{{ alertDateGroups.length }} day(s) · {{ alertList.length }} account(s) · click a day to see who is on it @if (alertUnreachableCount) { · {{ alertUnreachableCount }} customer(s) have no mobile or email }
@for (g of pagedDateGroups; track g.date) { } @empty { }
{{ alertDateHeading }} Customers Accounts Notified Then moves to Balance Action
{{ g.date | date: 'dd MMM yyyy' }} {{ g.customers }} {{ g.rows.length }} @if (!g.loaded) { … } @else if (g.notified === g.customers) { All {{ g.customers }} notified } @else if (g.notified === 0) { None notified yet } @else { {{ g.notified }} of {{ g.customers }} notified } @if (g.loaded && g.earlier) {
{{ g.earlier }} earlier notice(s) sent at previous stages
} @if (g.loaded && g.unreachable) {
{{ g.unreachable }} cannot be reached by SMS / email
}
{{ g.after.label }}
@if (g.after.date) {
on {{ g.after.date | date: 'dd MMM yyyy' }}
}
{{ g.balance ? '₹' + (g.balance | number: '1.0-0') : '—' }}
{{ statusLoading ? 'Loading…' : 'No account matches these filters.' }}
@if (alertDateGroups.length > 0) {
{{ dateEffPage * dateSize + 1 }}–{{ min(alertDateGroups.length, (dateEffPage + 1) * dateSize) }} of {{ alertDateGroups.length }} day(s) Rows per page Page {{ dateEffPage + 1 }} of {{ dateCount() }}
}
}
} @if (activeSection === 'notif') {
Notification Scheduler
{{ schedulerRows.length }}
Customers in scope
Expiring in 30 days + lapsed
{{ schedulerDueCount }}
Next notice due
SMS, Email or WhatsApp to send
{{ schedulerNeverContacted }}
Never contacted
No notice on record
{{ schedSelected.size }}
Selected
Ready to dispatch
Notice queue — sent in the background
Waiting {{ queueCounts['PENDING'] + queueCounts['SENDING'] }} · Sent {{ queueCounts['SENT'] }} · Failed {{ queueCounts['FAILED'] }} · Cancelled {{ queueCounts['CANCELLED'] }}
@if (queueError) {
{{ queueError }}
} @for (g of queueGroups; track g.date) { } @empty { }
Date Customers Notices Waiting Sent Failed Cancelled Action
{{ g.date | date: 'dd MMM yyyy' }} {{ g.customers }} {{ g.rows.length }} {{ g.waiting }} {{ g.sent }} {{ g.failed }} {{ g.cancelled }}
{{ queueLoading ? 'Loading…' : 'Nothing has been queued yet. Select customers on the Alert Dashboard and use Notify selected.' }}
Escalation ladder — one channel at a time
The next rung is the first one not yet done. Call and visit rungs are recorded from Notify.
@if (schedShow) { @for (s of schedulerRows; track s.row.clientId) { } @empty { }
Customer KYC Ladder Next step Last contact Action
@if (s.next?.sends) { }
{{ s.row.clientName }}
{{ s.row.accountNo }}
{{ s.row.expiryDate | date: 'dd MMM yyyy' }}
{{ s.row.daysOverdue < 0 ? -s.row.daysOverdue + ' days left' : s.row.daysOverdue + ' days overdue' }}
@for (st of s.steps; track st.label) { {{ st.state === 'done' ? '✓ ' : '' }}{{ st.label }} }
{{ s.next ? s.next.label : 'Ladder complete' }} {{ s.row.lastContactDate ? (s.row.lastContactDate | date: 'dd MMM yyyy') : 'Never' }} @if (s.next?.sends) { } @else if (s.next) { }
{{ statusLoading ? 'Loading…' : 'No customer is expiring or lapsed.' }}
}
} @if (activeSection === 'audit') {
Audit Inventory
@if (auditError) {
{{ auditError }}
} @if (auditDay) {
Notices sent on {{ auditDay | date: 'dd MMM yyyy' }}
{{ auditDayTotal }} notice(s) · click a row for the message the customer received
@if (auditDayError) {
{{ auditDayError }}
} @for (r of auditDayRows; track r.id; let i = $index) { @if (auditExpanded === r.id) { } } @empty { }
# Customer / account Channel Time Sent to Reference Delivery Officer
{{ auditDayPage * auditPageSize + i + 1 }}
{{ r.clientName }}
{{ r.accountNo }}
{{ auditChannelLabels[r.channel] || r.channel }} {{ r.contactedAt | date: 'HH:mm' }} {{ r.destinationMasked || '—' }} {{ r.providerReference || '—' }} {{ auditStatusLabels[r.deliveryStatus] || r.deliveryStatus }} {{ r.officerName || '—' }}
Note
{{ r.notes }}
@if (r.messageBody) {
Message sent to the customer
{{ r.messageBody }}
} @if (r.failureReason) {
Failure reason
{{ r.failureReason }}
}
{{ auditDayLoading ? 'Loading…' : 'No notice matches these filters on this day.' }}
Page {{ auditDayPage + 1 }} of {{ auditDayPageCount() }}
} @else if (auditView === 'days') {
{{ auditTotals.notifiedCustomers }}
Customers notified
of {{ auditTotals.customers }} tried, summed per day
{{ auditTotals.notices }}
Notices on record
{{ auditDays.length }} day(s)
{{ auditTotals.delivered }}
Delivered
Reached the customer
{{ auditTotals.failed }}
Failed
Gateway refused
{{ auditTotals.noContact }}
No contact detail
No mobile / email on file
{{ auditTotals.recorded }}
Recorded
Calls and visits
Notices sent, day by day
{{ auditDays.length }} day(s) · click a day to see who was sent what
@for (d of auditDays; track d.date) { } @empty { }
Date Customers notified Customers tried Notices Delivered Failed No contact detail Recorded (call / visit) Action
{{ d.date | date: 'dd MMM yyyy' }} {{ d.notifiedCustomers ?? 0 }} {{ d.customers }} {{ d.notices }} {{ d.delivered }} {{ d.failed }} {{ d.noContact + d.notConfigured }} {{ d.recorded }}
{{ auditLoading ? 'Loading…' : 'No notice matches these filters.' }}
} @else {
Customers contacted
{{ auditCustTotal }} customer(s) · most recently contacted first · History shows everything done for one
@for (c of auditCustomers; track c.clientId) { } @empty { }
Customer / account Notices Delivered Failed Not sent Recorded Latest Action
{{ c.clientName }}
{{ c.accountNo }}
{{ c.notices }} {{ c.delivered }} {{ c.failed }} {{ c.notSent }} {{ c.recorded }}
{{ c.lastAt | date: 'dd MMM yyyy, HH:mm' }}
{{ auditChannelLabels[c.lastChannel] || c.lastChannel }} · {{ auditStatusLabels[c.lastStatus] || c.lastStatus }}
{{ auditCustLoading ? 'Loading…' : 'No customer matches these filters.' }}
Page {{ auditCustPage + 1 }} of {{ auditCustPageCount() }}
}
} @if (activeSection === 'exceptions') {
KYC Exceptions Dashboard
{{ exceptionRows.length }}
Pending Exceptions
Immediate attention
{{ notYetContactedCount }}
Not Yet Contacted
Action required
{{ panMismatchesCount }}
PAN Mismatches
Requires physical review
{{ aadhaarFailuresCount }}
Aadhaar Failures
Re-KYC OTP needed
Exceptions Register
@if (activeExceptionSubTab === 'pending') { @if (pendingExceptions.length === 0) {
🎉
All exceptions resolved!
There are no active client KYC exceptions on record.
} @else { @for (c of pendingExceptions; track c.id) { }
Client / Account Branch Exception Reason & Date Risk Contact Status Contact Logs Actions
{{ c.name }}
{{ c.account }}
{{ c.contact.branch }}
{{ c.exceptionReason }}
Opened: {{ c.exceptionDate }}
{{ c.riskLabel }} @if (c.contactHistory && c.contactHistory.length > 0) { Contacted
Last: {{ c.contactHistory[c.contactHistory.length - 1].date }}
} @else { Not Contacted }
@if (c.contactHistory && c.contactHistory.length > 0) {
@for (log of c.contactHistory; track $index) {
{{ log.date }} ({{ log.action }}): {{ log.notes }} — {{ log.officer }} @if (log.platformSent) { @if (log.delivered) { Sent {{ log.destinationMasked }} } @else { Not sent } }
}
} @else { No logs recorded }
} } @if (activeExceptionSubTab === 'resolved') { @if (resolvedExceptions.length === 0) {
📋
No resolved exceptions
Resolved KYC exceptions will appear here once verification is completed.
} @else { @for (c of resolvedExceptions; track c.id) { }
Client / Account Branch Exception Reason & Resolution Risk Contact Logs Status
{{ c.name }}
{{ c.account }}
{{ c.contact.branch }}
{{ c.exceptionReason }}
Resolved On: {{ c.exceptionResolvedOn }}
{{ c.riskLabel }} @if (c.contactHistory && c.contactHistory.length > 0) {
@for (log of c.contactHistory; track $index) {
{{ log.date }} ({{ log.action }}): {{ log.notes }} — {{ log.officer }} @if (log.platformSent) { @if (log.delivered) { Sent {{ log.destinationMasked }} } @else { Not sent } }
}
} @else { No logs recorded }
Exception Resolved
} }
}
@if (queueDetail; as d) {
Notices queued on {{ d.date | date: 'dd MMM yyyy' }}
{{ d.customers }} customer(s) · {{ d.rows.length }} notice(s) — Waiting {{ d.waiting }} · Sent {{ d.sent }} · Failed {{ d.failed }} · Cancelled {{ d.cancelled }}
✕
@if (queueFailed(d).length) {
Not delivered — contact these {{ queueFailed(d).length }} customer(s) manually
@for (q of queueFailed(d); track q.id) { }
Customer Account Channel Why it failed Action
{{ q.clientName }}
{{ q.accountNo }} {{ q.channel }} {{ q.resultMessage || '—' }}
} @for (q of pgSlice('queue', queueOther(d)); track q.id; let i = $index) { }
# Customer Account Notice Channel Queued Send at Status Result Action
{{ pgFrom('queue') + i + 1 }}
{{ q.clientName }}
{{ q.accountNo }} {{ q.stageLabel || (q.noticeType === 'DORMANCY' ? 'Account notice' : 'Re-KYC notice') }} {{ q.channel }} {{ q.createdAt | date: 'HH:mm' }} {{ q.scheduledFor | date: 'dd MMM, HH:mm' }} {{ q.status === 'PENDING' ? 'Waiting' : q.status === 'SENDING' ? 'Sending' : q.status === 'SENT' ? 'Sent' : q.status === 'FAILED' ? 'Failed' : 'Cancelled' }} @if (q.attempts > 1 && q.status === 'PENDING') {
retry {{ q.attempts }}
}
{{ q.resultMessage || '—' }} @if (q.status === 'PENDING') { } @else if (q.status === 'FAILED') { }
} @if (bulkOpen) {
Notify selected customers
{{ bulkDone && bulkQueued ? bulkQueued.queued : bulkTargets.length }} customers
✕
@if (!bulkDone && !bulkSending) {
A notice is sent to each customer once, even if several of their accounts are ticked.
@if (bulkUnreachable) {
{{ bulkUnreachable }} of the ticked customer(s) have no {{ bulkChannel === 'EMAIL' ? 'email address' : 'mobile number' }} on file and are left out. Use the "No mobile / email" filter to find them and reach them by call or visit.
} @if (bulkAlreadyDone) {
{{ bulkAlreadyDone }} of the ticked customer(s) already received a {{ bulkChannel }} notice.
}
@if (bulkError) {
{{ bulkError }}
} @for (g of bulkNoticeSummary; track g.label) {
{{ g.count }} customer(s): {{ g.label }} — worded for the stage each one is at.
}
@if (bulkTargets.length > bulkLimit) {
{{ bulkTargets.length }} customers selected — send at most {{ bulkLimit }} at a time.
} } @else { @if (bulkSending) {
Queuing…
} @else if (bulkQueued) {
{{ bulkQueued.queued }} notice(s) queued — they are sent in the background. @if (bulkQueued.skipped) { {{ bulkQueued.skipped }} already waiting in the queue, so not queued again. } @if (bulkQueued.unreachable) { {{ bulkQueued.unreachable }} have no mobile number / email, so nothing was queued for them. } Follow them in the Notification Scheduler tab.
} }
} @if (schedOpen) {
Dispatch next notice
{{ schedDone && schedQueued ? schedQueued.queued : schedTargets.length }} customers
✕
@if (!schedDone && !schedSending) { @for (t of pgSlice('sched', schedTargets); track t.row.clientId) { }
Customer Goes out as
{{ t.row.clientName }}
{{ t.row.accountNo }}
{{ t.next?.label }}
@if (schedError) {
{{ schedError }}
} @if (schedTargets.length > bulkLimit) {
Send at most {{ bulkLimit }} at a time.
} } @else { @if (schedSending) {
Queuing…
} @else if (schedQueued) {
{{ schedQueued.queued }} notice(s) queued — they are sent in the background and appear in the queue below. @if (schedQueued.skipped) { {{ schedQueued.skipped }} already waiting, so not queued again. } @if (schedQueued.unreachable) { {{ schedQueued.unreachable }} have no mobile number / email, so nothing was queued for them. }
} }
} @if (historyFor; as h) {
{{ h.name }} — what has been done
{{ h.account }} · {{ (historyRows[h.clientId] || []).length }} action(s), newest first
✕
@for (x of pgSlice('history', historyRows[h.clientId] || []); track $index) { } @empty { }
When Action Sent to Result Officer Note
{{ x.when }} {{ x.action }} {{ x.to || '—' }} {{ x.delivered ? 'Done' : 'Not delivered' }}
{{ x.result }}
{{ x.officer || '—' }} {{ x.notes }}
{{ historyLoading ? 'Loading…' : 'Nothing has been done for this customer yet.' }}
} @if (contactDialogOpen && current) {
{{ current.av }}
Log Customer Contact
{{ current.name }} · {{ current.account }}
✕
Customer Contact Info
{{ current.name }}
{{ current.contact.mobile }}
{{ current.contact.email }}
@if (selectedChannelSends) {
A KYC notice will be sent to the customer when you save.
} @else {
Records an action you already took. Nothing is sent to the customer.
} @if (selectedChannelSends && contactPreview) {
{{ contactPreview.subject }} {{ contactPreview.body }}
} @if (selectedChannelUnavailable) {
This customer has no {{ selectedChannel?.requires === 'email' ? 'email address' : 'mobile number' }} on file — choose a different channel.
}
@if (contactError) { }
} @if (resolveDialogOpen && current) {
{{ current.av }}
Resolve KYC Exception
{{ current.name }} · {{ current.account }}
✕
Pending Reason: {{ current.exceptionReason }} @if (current.exceptionRemarks) {
"{{ current.exceptionRemarks }}"
}
@if (showPanResolution) {
@if (resolvePanActive) {
@if (resolvePanNumber && resolvePanNumber.length !== 10) {
PAN must be exactly 10 characters.
}
}
} @if (showAadhaarResolution) {
@if (resolveAadhaarActive) {
@if (resolveAadhaarNumber && resolveAadhaarNumber.length !== 12) {
Aadhaar must be exactly 12 digits.
}
}
} @if (showGeneralResolution) {
@if (resolveGeneralActive) {
}
}
}
{{ toastMsg }}
@if (total > 0) {
{{ pgFrom(key) + 1 }}–{{ min(total, pgFrom(key) + pgSize(key)) }} of {{ total }} Rows per page Page {{ pgPage(key, total) + 1 }} of {{ pgCount(key, total) }}
}