Merge branch 'feature/merge-updates' from cbsnexora into feature/ui-reports-product

Conflict resolutions:
- src/main.scss: kept both new dialog backdrops (CIC/PSL/ALM/CIMS/CKYC/Acct)
  and incoming dialog panel styles (.abd-dialog-panel, .arc-dialog-panel)
- client-general-step.component.html/.ts: kept HEAD's agriculture/non-agri
  income fields, education/degree/religion/social-category dropdowns,
  and inject() refactor matching the incoming pattern
- kyc-management.component.html/.scss/.ts: kept HEAD's Home Visit
  terminology, takeAction() method, hv-* and vkyc-* dialog styles;
  accepted incoming .km-warn-d style addition

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-29 19:22:00 +05:30
co-authored by Claude Opus 4.7
470 changed files with 46834 additions and 11014 deletions
@@ -120,284 +120,277 @@
<!-- SECTION 1: ZIP Details -->
@if (activeSection === 'zip') {
<div class="cv-section">
<div class="cv-section__head">
<div class="cv-section__title">
<fa-icon icon="file-archive"></fa-icon>
<div>
<h3>ZIP Batch Details</h3>
<p>CERSAI ZIP archives grouped by creation date</p>
</div>
</div>
<span class="cv-badge cv-badge--blue">Phase 2</span>
</div>
@if (loading) {
<div class="cv-state cv-state--loading">
<fa-icon icon="spinner" animation="spin"></fa-icon>
<span>Loading batches…</span>
</div>
}
@if (error && !loading) {
<div class="cv-state cv-state--error">
<fa-icon icon="exclamation-triangle"></fa-icon>
<span>{{ error }}</span>
</div>
}
@if (!loading && !error && groups.length === 0) {
<div class="cv-state cv-state--empty">
<fa-icon icon="file-archive" size="2x"></fa-icon>
<h4>No batches found</h4>
<p>Create a batch from the Record Master to see it here.</p>
</div>
}
@if (!loading && groups.length > 0) {
<div class="cv-groups">
@for (group of groups; track group.dateLabel) {
<div class="cv-group" [class.cv-group--open]="group.expanded">
<button class="cv-group__head" (click)="toggleGroup(group)">
<div class="cv-group__left">
<fa-icon [icon]="group.expanded ? 'chevron-down' : 'chevron-right'" class="cv-group__chev"></fa-icon>
<div class="cv-group__date-pill">
<fa-icon icon="calendar-check"></fa-icon>
<span>{{ group.dateLabel }}</span>
</div>
<div class="cv-group__summary">
<span><strong>{{ group.batchCount }}</strong> batches</span>
<span class="cv-group__sep">·</span>
<span><strong>{{ group.totalRecordCount }}</strong> records</span>
<span class="cv-group__sep">·</span>
<span>{{ formatBytes(group.totalSizeBytes) }}</span>
</div>
</div>
<div class="cv-group__right">
@if (group.pendingCount > 0) {
<span class="cv-pill cv-pill--amber">
<fa-icon icon="hourglass-half"></fa-icon> {{ group.pendingCount }} pending
</span>
}
@if (group.uploadedCount > 0) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-double"></fa-icon> {{ group.uploadedCount }} uploaded
</span>
}
</div>
</button>
@if (group.expanded) {
<div class="cv-group__body">
<table class="cv-batches">
<thead>
<tr>
<th>Batch ID</th>
<th>Filename</th>
<th class="cv-batches__num">Records</th>
<th class="cv-batches__num">Size</th>
<th>Status</th>
<th class="cv-batches__actions">Actions</th>
</tr>
</thead>
<tbody>
@for (batch of group.batches; track trackBatch($index, batch)) {
<tr>
<td class="cv-batches__bid">
<fa-icon icon="tag"></fa-icon>
<span>{{ batch.batchId }}</span>
</td>
<td class="cv-batches__file">
<fa-icon icon="file-archive"></fa-icon>
<span>{{ batch.filename }}</span>
</td>
<td class="cv-batches__num">{{ batch.recordCount }}</td>
<td class="cv-batches__num">{{ formatBytes(batch.sizeBytes) }}</td>
<td>
@if (batch.uploadedToCersai) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-circle"></fa-icon> Uploaded
</span>
} @else {
<span class="cv-pill cv-pill--amber">
<fa-icon icon="hourglass-half"></fa-icon> Pending
</span>
}
</td>
<td class="cv-batches__actions">
<button
class="cv-action-btn"
(click)="openManifest(batch, $event)"
title="View manifest"
>
<fa-icon icon="eye"></fa-icon>
</button>
@if (batch.documentId) {
<button
class="cv-action-btn cv-action-btn--primary"
(click)="downloadZip(batch, $event)"
title="Download ZIP"
>
<fa-icon icon="download"></fa-icon>
</button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="cv-section">
<div class="cv-section__head">
<div class="cv-section__title">
<fa-icon icon="file-archive"></fa-icon>
<div>
<h3>ZIP Batch Details</h3>
<p>CERSAI ZIP archives grouped by creation date</p>
</div>
}
</div>
<span class="cv-badge cv-badge--blue">Phase 2</span>
</div>
}
</div>
@if (loading) {
<div class="cv-state cv-state--loading">
<fa-icon icon="spinner" animation="spin"></fa-icon>
<span>Loading batches…</span>
</div>
}
@if (error && !loading) {
<div class="cv-state cv-state--error">
<fa-icon icon="exclamation-triangle"></fa-icon>
<span>{{ error }}</span>
</div>
}
@if (!loading && !error && groups.length === 0) {
<div class="cv-state cv-state--empty">
<fa-icon icon="file-archive" size="2x"></fa-icon>
<h4>No batches found</h4>
<p>Create a batch from the Record Master to see it here.</p>
</div>
}
@if (!loading && groups.length > 0) {
<div class="cv-groups">
@for (group of groups; track group.dateLabel) {
<div class="cv-group" [class.cv-group--open]="group.expanded">
<button class="cv-group__head" (click)="toggleGroup(group)">
<div class="cv-group__left">
<fa-icon [icon]="group.expanded ? 'chevron-down' : 'chevron-right'" class="cv-group__chev"></fa-icon>
<div class="cv-group__date-pill">
<fa-icon icon="calendar-check"></fa-icon>
<span>{{ group.dateLabel }}</span>
</div>
<div class="cv-group__summary">
<span
><strong>{{ group.batchCount }}</strong> batches</span
>
<span class="cv-group__sep">·</span>
<span
><strong>{{ group.totalRecordCount }}</strong> records</span
>
<span class="cv-group__sep">·</span>
<span>{{ formatBytes(group.totalSizeBytes) }}</span>
</div>
</div>
<div class="cv-group__right">
@if (group.pendingCount > 0) {
<span class="cv-pill cv-pill--amber">
<fa-icon icon="hourglass-half"></fa-icon> {{ group.pendingCount }} pending
</span>
}
@if (group.uploadedCount > 0) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-double"></fa-icon> {{ group.uploadedCount }} uploaded
</span>
}
</div>
</button>
@if (group.expanded) {
<div class="cv-group__body">
<table class="cv-batches">
<thead>
<tr>
<th>Batch ID</th>
<th>Filename</th>
<th class="cv-batches__num">Records</th>
<th class="cv-batches__num">Size</th>
<th>Status</th>
<th class="cv-batches__actions">Actions</th>
</tr>
</thead>
<tbody>
@for (batch of group.batches; track trackBatch($index, batch)) {
<tr>
<td class="cv-batches__bid">
<fa-icon icon="tag"></fa-icon>
<span>{{ batch.batchId }}</span>
</td>
<td class="cv-batches__file">
<fa-icon icon="file-archive"></fa-icon>
<span>{{ batch.filename }}</span>
</td>
<td class="cv-batches__num">{{ batch.recordCount }}</td>
<td class="cv-batches__num">{{ formatBytes(batch.sizeBytes) }}</td>
<td>
@if (batch.uploadedToCersai) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-circle"></fa-icon> Uploaded
</span>
} @else {
<span class="cv-pill cv-pill--amber">
<fa-icon icon="hourglass-half"></fa-icon> Pending
</span>
}
</td>
<td class="cv-batches__actions">
<button class="cv-action-btn" (click)="openManifest(batch, $event)" title="View manifest">
<fa-icon icon="eye"></fa-icon>
</button>
@if (batch.documentId) {
<button
class="cv-action-btn cv-action-btn--primary"
(click)="downloadZip(batch, $event)"
title="Download ZIP"
>
<fa-icon icon="download"></fa-icon>
</button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
}
</div>
}
</div>
}
<!-- SECTION 2: SFTP Upload + API Match -->
@if (activeSection === 'sftp') {
<div class="cv-section">
<div class="cv-section__head">
<div class="cv-section__title">
<fa-icon icon="cloud-upload-alt"></fa-icon>
<div>
<h3>SFTP Upload &amp; ACK · API Match</h3>
<p>CERSAI SFTP transfer history with ACK receipts and per-batch logs</p>
<div class="cv-section">
<div class="cv-section__head">
<div class="cv-section__title">
<fa-icon icon="cloud-upload-alt"></fa-icon>
<div>
<h3>SFTP Upload &amp; ACK · API Match</h3>
<p>CERSAI SFTP transfer history with ACK receipts and per-batch logs</p>
</div>
</div>
<span class="cv-badge cv-badge--amber">Phase 3-4</span>
</div>
<div class="cv-sftp-kpis">
<div class="cv-sftp-kpi cv-sftp-kpi--green">
<div class="cv-sftp-kpi__icon"><fa-icon icon="check-circle"></fa-icon></div>
<div>
<div class="cv-sftp-kpi__value">{{ sftpSuccessCount }}</div>
<div class="cv-sftp-kpi__label">Uploaded successfully</div>
</div>
</div>
<div class="cv-sftp-kpi cv-sftp-kpi--red">
<div class="cv-sftp-kpi__icon"><fa-icon icon="times-circle"></fa-icon></div>
<div>
<div class="cv-sftp-kpi__value">{{ sftpFailedCount }}</div>
<div class="cv-sftp-kpi__label">Failed</div>
</div>
</div>
<div class="cv-sftp-kpi cv-sftp-kpi--amber">
<div class="cv-sftp-kpi__icon"><fa-icon icon="hourglass-half"></fa-icon></div>
<div>
<div class="cv-sftp-kpi__value">{{ sftpPendingCount }}</div>
<div class="cv-sftp-kpi__label">Pending upload</div>
</div>
</div>
</div>
<span class="cv-badge cv-badge--amber">Phase 3-4</span>
</div>
<div class="cv-sftp-kpis">
<div class="cv-sftp-kpi cv-sftp-kpi--green">
<div class="cv-sftp-kpi__icon"><fa-icon icon="check-circle"></fa-icon></div>
<div>
<div class="cv-sftp-kpi__value">{{ sftpSuccessCount }}</div>
<div class="cv-sftp-kpi__label">Uploaded successfully</div>
@if (loading) {
<div class="cv-state cv-state--loading">
<fa-icon icon="spinner" animation="spin"></fa-icon>
<span>Loading logs…</span>
</div>
</div>
<div class="cv-sftp-kpi cv-sftp-kpi--red">
<div class="cv-sftp-kpi__icon"><fa-icon icon="times-circle"></fa-icon></div>
<div>
<div class="cv-sftp-kpi__value">{{ sftpFailedCount }}</div>
<div class="cv-sftp-kpi__label">Failed</div>
}
@if (!loading && sftpLogs.length === 0) {
<div class="cv-state cv-state--empty">
<fa-icon icon="cloud-upload-alt" size="2x"></fa-icon>
<h4>No SFTP upload history yet</h4>
<p>Upload a batch from the Record Master to see CERSAI transfer logs here.</p>
</div>
</div>
<div class="cv-sftp-kpi cv-sftp-kpi--amber">
<div class="cv-sftp-kpi__icon"><fa-icon icon="hourglass-half"></fa-icon></div>
<div>
<div class="cv-sftp-kpi__value">{{ sftpPendingCount }}</div>
<div class="cv-sftp-kpi__label">Pending upload</div>
</div>
</div>
</div>
}
@if (loading) {
<div class="cv-state cv-state--loading">
<fa-icon icon="spinner" animation="spin"></fa-icon>
<span>Loading logs…</span>
</div>
}
@if (!loading && sftpLogs.length === 0) {
<div class="cv-state cv-state--empty">
<fa-icon icon="cloud-upload-alt" size="2x"></fa-icon>
<h4>No SFTP upload history yet</h4>
<p>Upload a batch from the Record Master to see CERSAI transfer logs here.</p>
</div>
}
@if (!loading && sftpLogs.length > 0) {
<div class="cv-sftp-logs">
@for (log of sftpLogs; track log.id) {
<div
class="cv-sftp-log"
[class.cv-sftp-log--success]="isSftpSuccess(log)"
[class.cv-sftp-log--failed]="isSftpFailed(log)"
>
<div class="cv-sftp-log__icon">
@if (isSftpSuccess(log)) {
<fa-icon icon="check-circle"></fa-icon>
} @else if (isSftpFailed(log)) {
<fa-icon icon="times-circle"></fa-icon>
} @else {
<fa-icon icon="hourglass-half"></fa-icon>
}
</div>
<div class="cv-sftp-log__body">
<div class="cv-sftp-log__head">
<span class="cv-sftp-log__bid">
<fa-icon icon="tag"></fa-icon> {{ log.batchId }}
</span>
@if (!loading && sftpLogs.length > 0) {
<div class="cv-sftp-logs">
@for (log of sftpLogs; track log.id) {
<div
class="cv-sftp-log"
[class.cv-sftp-log--success]="isSftpSuccess(log)"
[class.cv-sftp-log--failed]="isSftpFailed(log)"
>
<div class="cv-sftp-log__icon">
@if (isSftpSuccess(log)) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-circle"></fa-icon> Upload successful
</span>
<fa-icon icon="check-circle"></fa-icon>
} @else if (isSftpFailed(log)) {
<span class="cv-pill cv-pill--red">
<fa-icon icon="times-circle"></fa-icon> Upload failed
</span>
<fa-icon icon="times-circle"></fa-icon>
} @else {
<span class="cv-pill cv-pill--amber">
<fa-icon icon="hourglass-half"></fa-icon> Pending
</span>
<fa-icon icon="hourglass-half"></fa-icon>
}
<span class="cv-sftp-log__time">
<fa-icon icon="clock"></fa-icon>
{{ log.uploadedOnLabel || log.createdOnLabel || '—' }}
</span>
</div>
<div class="cv-sftp-log__body">
<div class="cv-sftp-log__head">
<span class="cv-sftp-log__bid"> <fa-icon icon="tag"></fa-icon> {{ log.batchId }} </span>
@if (isSftpSuccess(log)) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-circle"></fa-icon> Upload successful
</span>
} @else if (isSftpFailed(log)) {
<span class="cv-pill cv-pill--red"> <fa-icon icon="times-circle"></fa-icon> Upload failed </span>
} @else {
<span class="cv-pill cv-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
}
<span class="cv-sftp-log__time">
<fa-icon icon="clock"></fa-icon>
{{ log.uploadedOnLabel || log.createdOnLabel || '—' }}
</span>
</div>
<div class="cv-sftp-log__meta">
<div>
<span class="cv-sftp-log__label">Filename</span>
<span class="cv-sftp-log__value">{{ log.filename }}</span>
</div>
<div>
<span class="cv-sftp-log__label">Records</span>
<span class="cv-sftp-log__value">{{ log.recordCount }}</span>
</div>
<div>
<span class="cv-sftp-log__label">Size</span>
<span class="cv-sftp-log__value">{{ formatBytes(log.sizeBytes) }}</span>
</div>
@if (log.cersaiReference) {
<div class="cv-sftp-log__meta">
<div>
<span class="cv-sftp-log__label">CERSAI Ref</span>
<span class="cv-sftp-log__value cv-sftp-log__value--mono">{{ log.cersaiReference }}</span>
<span class="cv-sftp-log__label">Filename</span>
<span class="cv-sftp-log__value">{{ log.filename }}</span>
</div>
<div>
<span class="cv-sftp-log__label">Records</span>
<span class="cv-sftp-log__value">{{ log.recordCount }}</span>
</div>
<div>
<span class="cv-sftp-log__label">Size</span>
<span class="cv-sftp-log__value">{{ formatBytes(log.sizeBytes) }}</span>
</div>
@if (log.cersaiReference) {
<div>
<span class="cv-sftp-log__label">CERSAI Ref</span>
<span class="cv-sftp-log__value cv-sftp-log__value--mono">{{ log.cersaiReference }}</span>
</div>
}
</div>
@if (log.uploadRemarks) {
<div class="cv-sftp-log__remarks" [class.cv-sftp-log__remarks--err]="isSftpFailed(log)">
<fa-icon [icon]="isSftpFailed(log) ? 'exclamation-triangle' : 'info-circle'"></fa-icon>
<span>{{ log.uploadRemarks }}</span>
</div>
}
</div>
@if (log.uploadRemarks) {
<div class="cv-sftp-log__remarks" [class.cv-sftp-log__remarks--err]="isSftpFailed(log)">
<fa-icon [icon]="isSftpFailed(log) ? 'exclamation-triangle' : 'info-circle'"></fa-icon>
<span>{{ log.uploadRemarks }}</span>
</div>
}
</div>
<div class="cv-sftp-log__actions">
<button class="cv-action-btn" (click)="openManifest(log, $event)" title="View manifest">
<fa-icon icon="eye"></fa-icon>
</button>
@if (log.documentId) {
<button
class="cv-action-btn cv-action-btn--primary"
(click)="downloadZip(log, $event)"
title="Download ZIP"
>
<fa-icon icon="download"></fa-icon>
<div class="cv-sftp-log__actions">
<button class="cv-action-btn" (click)="openManifest(log, $event)" title="View manifest">
<fa-icon icon="eye"></fa-icon>
</button>
}
@if (log.documentId) {
<button
class="cv-action-btn cv-action-btn--primary"
(click)="downloadZip(log, $event)"
title="Download ZIP"
>
<fa-icon icon="download"></fa-icon>
</button>
}
</div>
</div>
</div>
}
</div>
}
</div>
}
</div>
}
</div>
}
<!-- Manifest modal -->
@@ -451,13 +444,9 @@
<div>
<span class="cv-kpi__label">Status</span>
@if (manifestBatch.uploadedToCersai) {
<span class="cv-pill cv-pill--green">
<fa-icon icon="check-circle"></fa-icon> Uploaded
</span>
<span class="cv-pill cv-pill--green"> <fa-icon icon="check-circle"></fa-icon> Uploaded </span>
} @else {
<span class="cv-pill cv-pill--amber">
<fa-icon icon="hourglass-half"></fa-icon> Pending
</span>
<span class="cv-pill cv-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
}
</div>
<div class="cv-manifest-head__grid-span">
@@ -526,7 +515,10 @@
class="cv-dot"
[class.cv-dot--ok]="entry.idProof?.packaged"
[class.cv-dot--miss]="!entry.idProof?.packaged"
[title]="(entry.idProof?.type || 'ID') + (entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '')"
[title]="
(entry.idProof?.type || 'ID') +
(entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '')
"
>
<fa-icon [icon]="entry.idProof?.packaged ? 'check' : 'times'"></fa-icon>
</span>
@@ -536,13 +528,23 @@
class="cv-dot"
[class.cv-dot--ok]="entry.addressProof?.packaged"
[class.cv-dot--miss]="!entry.addressProof?.packaged"
[title]="(entry.addressProof?.type || 'Address') + (entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '')"
[title]="
(entry.addressProof?.type || 'Address') +
(entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '')
"
>
<fa-icon [icon]="entry.addressProof?.packaged ? 'check' : 'times'"></fa-icon>
</span>
</td>
<td class="cv-td-r cv-manifest-size">
{{ formatBytes((entry.photo?.bytes || 0) + (entry.signature?.bytes || 0) + (entry.idProof?.bytes || 0) + (entry.addressProof?.bytes || 0)) }}
{{
formatBytes(
(entry.photo?.bytes || 0) +
(entry.signature?.bytes || 0) +
(entry.idProof?.bytes || 0) +
(entry.addressProof?.bytes || 0)
)
}}
</td>
</tr>
}
@@ -554,9 +556,7 @@
</div>
<div class="cv-modal__footer">
<button class="cv-btn cv-btn--ghost" (click)="closeManifest()">
<fa-icon icon="times"></fa-icon> Close
</button>
<button class="cv-btn cv-btn--ghost" (click)="closeManifest()"><fa-icon icon="times"></fa-icon> Close</button>
@if (manifestBatch?.documentId) {
<button class="cv-btn cv-btn--primary" (click)="downloadZip(manifestBatch, $event)">
<fa-icon icon="download"></fa-icon>
@@ -91,14 +91,18 @@ $purple-light: #efeafb;
cursor: pointer;
transition: all 0.15s;
fa-icon { font-size: 12px; }
fa-icon {
font-size: 12px;
}
&:hover:not(:disabled) {
background: $gray-50;
border-color: $gray-500;
}
&--ghost { /* default */ }
&--ghost {
/* default */
}
&--primary {
background: $primary;
@@ -139,13 +143,27 @@ $purple-light: #efeafb;
justify-content: center;
font-size: 15px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
&--blue { background: linear-gradient(135deg, $primary, #4285f4); }
&--teal { background: linear-gradient(135deg, $teal-700, $green); }
&--amber { background: linear-gradient(135deg, $amber, #d4892f); }
&--green { background: linear-gradient(135deg, $green, #2fb88c); }
&--purple { background: linear-gradient(135deg, $purple, #8a6fd4); }
&--blue {
background: linear-gradient(135deg, $primary, #4285f4);
}
&--teal {
background: linear-gradient(135deg, $teal-700, $green);
}
&--amber {
background: linear-gradient(135deg, $amber, #d4892f);
}
&--green {
background: linear-gradient(135deg, $green, #2fb88c);
}
&--purple {
background: linear-gradient(135deg, $purple, #8a6fd4);
}
}
&__label {
@@ -209,7 +227,10 @@ $purple-light: #efeafb;
align-items: center;
gap: 6px;
fa-icon { color: $primary; font-size: 11px; }
fa-icon {
color: $primary;
font-size: 11px;
}
}
.cv-input {
@@ -251,7 +272,7 @@ select.cv-input {
border: 1px solid $gray-200 !important;
border-radius: 14px !important;
padding: 8px !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) !important;
box-shadow: 0 1px 3px rgb(0 0 0 / 3%) !important;
}
button.cv-tab,
@@ -283,7 +304,7 @@ button.cv-tab,
color: $gray-500 !important;
}
> span:not(.cv-tab__badge):not(.cv-tab__count) {
> span:not(.cv-tab__badge, .cv-tab__count) {
display: inline-flex !important;
align-items: center !important;
}
@@ -292,7 +313,9 @@ button.cv-tab,
background: $gray-50 !important;
color: $gray-900 !important;
> fa-icon { color: $primary !important; }
> fa-icon {
color: $primary !important;
}
}
&--active {
@@ -301,7 +324,9 @@ button.cv-tab,
color: $primary-dark !important;
box-shadow: 0 2px 8px rgba($primary, 0.12) !important;
> fa-icon { color: $primary !important; }
> fa-icon {
color: $primary !important;
}
}
}
@@ -342,13 +367,20 @@ button.cv-tab,
}
@media (width <= 680px) {
.cv-tabs { flex-direction: column !important; }
.cv-tabs {
flex-direction: column !important;
}
button.cv-tab,
.cv-tabs .cv-tab { justify-content: space-between !important; }
.cv-tabs .cv-tab {
justify-content: space-between !important;
}
}
/* Hide the per-section header badge since the tab strip already carries it */
.cv-section .cv-badge { display: none; }
.cv-section .cv-badge {
display: none;
}
/* ---------- Section card ---------- */
.cv-section {
@@ -412,8 +444,15 @@ button.cv-tab,
text-transform: uppercase;
letter-spacing: 0.3px;
&--blue { background: $primary-light; color: $primary-dark; }
&--amber { background: $amber-light; color: $amber; }
&--blue {
background: $primary-light;
color: $primary-dark;
}
&--amber {
background: $amber-light;
color: $amber;
}
}
/* ---------- State banners ---------- */
@@ -426,7 +465,9 @@ button.cv-tab,
border-radius: 10px;
font-size: 13px;
fa-icon { font-size: 18px; }
fa-icon {
font-size: 18px;
}
&--loading {
background: $primary-light;
@@ -445,9 +486,22 @@ button.cv-tab,
color: $gray-500;
border: 1px dashed $gray-200;
fa-icon { color: $primary; margin-bottom: 4px; }
h4 { margin: 0; font-size: 15px; font-weight: 600; color: $gray-900; }
p { margin: 0; font-size: 12px; }
fa-icon {
color: $primary;
margin-bottom: 4px;
}
h4 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: $gray-900;
}
p {
margin: 0;
font-size: 12px;
}
}
}
@@ -463,9 +517,13 @@ button.cv-tab,
border: 1px solid $gray-200;
border-radius: 10px;
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover { border-color: rgba($primary, 0.3); }
&:hover {
border-color: rgba($primary, 0.3);
}
&--open {
border-color: $primary-light;
@@ -485,7 +543,9 @@ button.cv-tab,
font-family: inherit;
text-align: left;
&:hover { background: $gray-100; }
&:hover {
background: $gray-100;
}
}
&__left {
@@ -513,7 +573,10 @@ button.cv-tab,
font-size: 12px;
font-weight: 700;
fa-icon { color: $primary; font-size: 11px; }
fa-icon {
color: $primary;
font-size: 11px;
}
}
&__summary {
@@ -523,10 +586,15 @@ button.cv-tab,
gap: 8px;
flex-wrap: wrap;
strong { color: $gray-900; font-weight: 700; }
strong {
color: $gray-900;
font-weight: 700;
}
}
&__sep { color: $gray-200; }
&__sep {
color: $gray-200;
}
&__right {
display: flex;
@@ -550,7 +618,9 @@ button.cv-tab,
font-size: 11px;
font-weight: 700;
fa-icon { font-size: 10px; }
fa-icon {
font-size: 10px;
}
&--green {
background: $green-light;
@@ -617,19 +687,31 @@ button.cv-tab,
&--green {
border-color: rgba($green, 0.3);
background: linear-gradient(180deg, $green-light, $white);
.cv-sftp-kpi__icon { background: $green; color: $white; }
.cv-sftp-kpi__icon {
background: $green;
color: $white;
}
}
&--red {
border-color: rgba($red, 0.3);
background: linear-gradient(180deg, $red-light, $white);
.cv-sftp-kpi__icon { background: $red; color: $white; }
.cv-sftp-kpi__icon {
background: $red;
color: $white;
}
}
&--amber {
border-color: rgba($amber, 0.3);
background: linear-gradient(180deg, $amber-light, $white);
.cv-sftp-kpi__icon { background: $amber; color: $white; }
.cv-sftp-kpi__icon {
background: $amber;
color: $white;
}
}
}
@@ -649,12 +731,22 @@ button.cv-tab,
border-left-width: 4px;
border-radius: 10px;
background: $white;
transition: background 0.15s, box-shadow 0.15s;
transition:
background 0.15s,
box-shadow 0.15s;
&:hover { background: #fafbfc; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); }
&:hover {
background: #fafbfc;
box-shadow: 0 2px 8px rgb(0 0 0 / 4%);
}
&--success { border-left-color: $green; }
&--failed { border-left-color: $red; }
&--success {
border-left-color: $green;
}
&--failed {
border-left-color: $red;
}
&__icon {
width: 36px;
@@ -669,10 +761,20 @@ button.cv-tab,
flex-shrink: 0;
}
&--success .cv-sftp-log__icon { background: $green-light; color: $green; }
&--failed .cv-sftp-log__icon { background: $red-light; color: $red; }
&--success .cv-sftp-log__icon {
background: $green-light;
color: $green;
}
&__body { flex: 1; min-width: 0; }
&--failed .cv-sftp-log__icon {
background: $red-light;
color: $red;
}
&__body {
flex: 1;
min-width: 0;
}
&__head {
display: flex;
@@ -688,7 +790,10 @@ button.cv-tab,
font-weight: 700;
color: $primary;
fa-icon { font-size: 11px; margin-right: 4px; }
fa-icon {
font-size: 11px;
margin-right: 4px;
}
}
&__time {
@@ -696,7 +801,10 @@ button.cv-tab,
color: $gray-500;
margin-left: auto;
fa-icon { font-size: 10px; margin-right: 4px; }
fa-icon {
font-size: 10px;
margin-right: 4px;
}
}
&__meta {
@@ -705,7 +813,12 @@ button.cv-tab,
gap: 8px 16px;
font-size: 12px;
> div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
> div {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
}
&__label {
@@ -739,14 +852,20 @@ button.cv-tab,
align-items: flex-start;
gap: 8px;
fa-icon { font-size: 12px; color: $gray-500; margin-top: 2px; }
fa-icon {
font-size: 12px;
color: $gray-500;
margin-top: 2px;
}
&--err {
background: $red-light;
border-color: rgba($red, 0.3);
color: $red;
fa-icon { color: $red; }
fa-icon {
color: $red;
}
}
}
@@ -763,7 +882,9 @@ button.cv-tab,
border-collapse: collapse;
font-size: 12px;
thead tr { background: $gray-50; }
thead tr {
background: $gray-50;
}
th {
text-align: left;
@@ -783,10 +904,20 @@ button.cv-tab,
vertical-align: middle;
}
tbody tr:hover { background: #f5f8fc; }
tbody tr:hover {
background: #f5f8fc;
}
&__num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
&__actions { text-align: right; width: 100px; }
&__num {
text-align: right;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
&__actions {
text-align: right;
width: 100px;
}
&__bid {
font-family: 'SF Mono', Consolas, monospace;
@@ -795,13 +926,21 @@ button.cv-tab,
color: $primary;
white-space: nowrap;
fa-icon { color: $primary; margin-right: 6px; font-size: 11px; }
fa-icon {
color: $primary;
margin-right: 6px;
font-size: 11px;
}
}
&__file {
color: $gray-700;
fa-icon { color: $primary; margin-right: 6px; font-size: 11px; }
fa-icon {
color: $primary;
margin-right: 6px;
font-size: 11px;
}
span {
word-break: break-all;
@@ -823,7 +962,9 @@ button.cv-tab,
margin-left: 4px;
transition: all 0.15s;
fa-icon { font-size: 12px; }
fa-icon {
font-size: 12px;
}
&:hover {
background: $primary-light;
@@ -900,7 +1041,7 @@ button.cv-tab,
.cv-modal {
position: fixed;
inset: 0;
background: rgba(15, 25, 45, 0.55);
background: rgb(15 25 45 / 55%);
backdrop-filter: blur(3px);
z-index: 100;
display: flex;
@@ -917,7 +1058,7 @@ button.cv-tab,
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
box-shadow: 0 24px 60px rgb(0 0 0 / 25%);
}
&__header {
@@ -1051,7 +1192,10 @@ button.cv-tab,
align-items: center;
gap: 6px;
fa-icon { color: $primary; font-size: 12px; }
fa-icon {
color: $primary;
font-size: 12px;
}
}
.cv-manifest-table-wrap {
@@ -1091,14 +1235,31 @@ button.cv-tab,
vertical-align: middle;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #f5f8fc; }
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: #f5f8fc;
}
}
.cv-th-c { text-align: center !important; width: 38px; }
.cv-th-r { text-align: right !important; }
.cv-td-c { text-align: center; }
.cv-td-r { text-align: right; }
.cv-th-c {
text-align: center !important;
width: 38px;
}
.cv-th-r {
text-align: right !important;
}
.cv-td-c {
text-align: center;
}
.cv-td-r {
text-align: right;
}
.cv-manifest-client {
display: flex;
@@ -1117,7 +1278,10 @@ button.cv-tab,
color: $gray-500;
font-family: 'SF Mono', Consolas, monospace;
fa-icon { font-size: 8px; margin-right: 2px; }
fa-icon {
font-size: 8px;
margin-right: 2px;
}
}
.cv-dot {
@@ -1157,11 +1321,21 @@ button.cv-tab,
/* ---------- Responsive ---------- */
@media (width <= 1100px) {
.cv-kpis { grid-template-columns: repeat(3, 1fr); }
.cv-kpis {
grid-template-columns: repeat(3, 1fr);
}
}
@media (width <= 700px) {
.cv-kpis { grid-template-columns: repeat(2, 1fr); }
.cv-filters__grid { grid-template-columns: 1fr; }
.cv-manifest-head__grid { grid-template-columns: 1fr; }
.cv-kpis {
grid-template-columns: repeat(2, 1fr);
}
.cv-filters__grid {
grid-template-columns: 1fr;
}
.cv-manifest-head__grid {
grid-template-columns: 1fr;
}
}
@@ -210,7 +210,11 @@ export class CkycVerificationComponent implements OnInit {
formatJavaDate(value: any): string {
if (!value) return '';
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d] = value;
const [
y,
m,
d
] = value;
return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`;
}
if (typeof value === 'string') return value;
@@ -221,7 +225,14 @@ export class CkycVerificationComponent implements OnInit {
if (value == null || value === '') return '';
const pad = (n: any) => String(n ?? 0).padStart(2, '0');
if (Array.isArray(value) && value.length >= 3) {
const [y, mo, d, h = 0, mi = 0, s = 0] = value;
const [
y,
mo,
d,
h = 0,
mi = 0,
s = 0
] = value;
const date = `${pad(d)}-${pad(mo)}-${y}`;
if (value.length >= 4) {
return `${date} ${pad(h)}:${pad(mi)}:${pad(s)}`;
@@ -269,7 +280,14 @@ export class CkycVerificationComponent implements OnInit {
private dateValue(value: any): number {
if (Array.isArray(value) && value.length >= 3) {
const [y, mo, d, h = 0, mi = 0, s = 0] = value;
const [
y,
mo,
d,
h = 0,
mi = 0,
s = 0
] = value;
return new Date(y, (mo || 1) - 1, d || 1, h, mi, s).getTime();
}
if (typeof value === 'string') {
@@ -293,10 +311,7 @@ export class CkycVerificationComponent implements OnInit {
/** Completion check per manifest entry */
isEntryComplete(entry: any): boolean {
return (
entry?.photo?.packaged &&
entry?.signature?.packaged &&
entry?.idProof?.packaged &&
entry?.addressProof?.packaged
entry?.photo?.packaged && entry?.signature?.packaged && entry?.idProof?.packaged && entry?.addressProof?.packaged
);
}
}
+9 -19
View File
@@ -29,9 +29,7 @@
<!-- 5-Phase pipeline banner -->
<div class="ckyc-pipeline">
<div class="ckyc-pipeline__head">
<span class="ckyc-pipeline__heading">
<fa-icon icon="stream"></fa-icon> 5-phase compliance pipeline
</span>
<span class="ckyc-pipeline__heading"> <fa-icon icon="stream"></fa-icon> 5-phase compliance pipeline </span>
<span class="ckyc-pipeline__hint">CBS → CKYC automation flow</span>
</div>
@@ -70,9 +68,7 @@
</div>
<!-- Menu cards -->
<h2 class="ckyc-dash__section-title">
<fa-icon icon="th-large"></fa-icon> Quick actions
</h2>
<h2 class="ckyc-dash__section-title"><fa-icon icon="th-large"></fa-icon> Quick actions</h2>
<div class="ckyc-dash__grid">
<a class="ckyc-card ckyc-card--blue" [routerLink]="['record-master']">
@@ -82,15 +78,15 @@
</div>
<div class="ckyc-card__body">
<div class="ckyc-card__title">Record Master</div>
<p class="ckyc-card__desc">Manage master records, identify clients without CKYC and collect missing artefacts.</p>
<p class="ckyc-card__desc">
Manage master records, identify clients without CKYC and collect missing artefacts.
</p>
<div class="ckyc-card__tags">
<span class="ckyc-tag">Phase 1</span>
<span class="ckyc-tag">Clients</span>
</div>
</div>
<div class="ckyc-card__cta">
Open <fa-icon icon="arrow-right"></fa-icon>
</div>
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
</a>
<a class="ckyc-card ckyc-card--teal" [routerLink]="['ckyc-verification']">
@@ -106,9 +102,7 @@
<span class="ckyc-tag">Batches</span>
</div>
</div>
<div class="ckyc-card__cta">
Open <fa-icon icon="arrow-right"></fa-icon>
</div>
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
</a>
<a class="ckyc-card ckyc-card--amber" [routerLink]="['ckyc-reports']">
@@ -124,9 +118,7 @@
<span class="ckyc-tag">MIS</span>
</div>
</div>
<div class="ckyc-card__cta">
Open <fa-icon icon="arrow-right"></fa-icon>
</div>
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
</a>
<a class="ckyc-card ckyc-card--purple" [routerLink]="['ekyc']">
@@ -142,9 +134,7 @@
<span class="ckyc-tag">Aadhaar</span>
</div>
</div>
<div class="ckyc-card__cta">
Open <fa-icon icon="arrow-right"></fa-icon>
</div>
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
</a>
</div>
</div>
+70 -24
View File
@@ -45,7 +45,7 @@ $purple-bg: #f8f5ff;
margin-bottom: 22px;
position: relative;
overflow: hidden;
box-shadow: 0 6px 24px rgba(26, 115, 232, 0.2);
box-shadow: 0 6px 24px rgb(26 115 232 / 20%);
&::before {
content: '';
@@ -55,7 +55,7 @@ $purple-bg: #f8f5ff;
width: 320px;
height: 320px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15), transparent 70%);
background: radial-gradient(circle, rgb(255 255 255 / 15%), transparent 70%);
pointer-events: none;
}
@@ -71,8 +71,8 @@ $purple-bg: #f8f5ff;
width: 58px;
height: 58px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgb(255 255 255 / 18%);
border: 1px solid rgb(255 255 255 / 30%);
display: flex;
align-items: center;
justify-content: center;
@@ -86,8 +86,8 @@ $purple-bg: #f8f5ff;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgb(255 255 255 / 15%);
border: 1px solid rgb(255 255 255 / 30%);
border-radius: 30px;
font-size: 12px;
font-weight: 600;
@@ -95,7 +95,10 @@ $purple-bg: #f8f5ff;
flex-shrink: 0;
position: relative;
fa-icon { color: #9fe1cb; font-size: 14px; }
fa-icon {
color: #9fe1cb;
font-size: 14px;
}
}
}
@@ -139,7 +142,9 @@ $purple-bg: #f8f5ff;
align-items: center;
gap: 8px;
fa-icon { color: $primary; }
fa-icon {
color: $primary;
}
}
&__hint {
@@ -155,8 +160,14 @@ $purple-bg: #f8f5ff;
overflow-x: auto;
padding: 4px 2px;
&::-webkit-scrollbar { height: 4px; }
&::-webkit-scrollbar-thumb { background: $gray-200; border-radius: 4px; }
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: $gray-200;
border-radius: 4px;
}
}
}
@@ -171,7 +182,9 @@ $purple-bg: #f8f5ff;
position: relative;
transition: transform 0.15s;
&:hover { transform: translateY(-2px); }
&:hover {
transform: translateY(-2px);
}
&__num {
position: absolute;
@@ -249,7 +262,9 @@ $purple-bg: #f8f5ff;
background: $amber;
}
.ckyc-phase__title { color: #633806; }
.ckyc-phase__title {
color: #633806;
}
}
&--wide {
@@ -270,7 +285,10 @@ $purple-bg: #f8f5ff;
text-transform: uppercase;
letter-spacing: 0.4px;
fa-icon { color: $primary; font-size: 14px; }
fa-icon {
color: $primary;
font-size: 14px;
}
}
/* ---------- Menu grid ---------- */
@@ -289,7 +307,10 @@ $purple-bg: #f8f5ff;
border-radius: 16px;
text-decoration: none;
color: $gray-900;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s,
border-color 0.2s;
cursor: pointer;
position: relative;
overflow: hidden;
@@ -304,12 +325,14 @@ $purple-bg: #f8f5ff;
border-radius: 50%;
opacity: 0.12;
pointer-events: none;
transition: transform 0.4s, opacity 0.3s;
transition:
transform 0.4s,
opacity 0.3s;
}
&:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgb(0 0 0 / 8%);
.ckyc-card__glow {
transform: scale(1.25);
@@ -320,7 +343,9 @@ $purple-bg: #f8f5ff;
background: $primary;
color: $white;
fa-icon { transform: translateX(3px); }
fa-icon {
transform: translateX(3px);
}
}
}
@@ -336,7 +361,7 @@ $purple-bg: #f8f5ff;
margin-bottom: 16px;
position: relative;
z-index: 1;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
box-shadow: 0 6px 18px rgb(0 0 0 / 10%);
}
&__body {
@@ -394,9 +419,11 @@ $purple-bg: #f8f5ff;
.ckyc-card__icon {
background: linear-gradient(135deg, $primary, #4285f4);
}
.ckyc-card__glow {
background: radial-gradient(circle, $primary, transparent 60%);
}
&:hover .ckyc-card__cta {
background: $primary;
border-color: $primary;
@@ -407,9 +434,11 @@ $purple-bg: #f8f5ff;
.ckyc-card__icon {
background: linear-gradient(135deg, $teal, #1d9e75);
}
.ckyc-card__glow {
background: radial-gradient(circle, $teal, transparent 60%);
}
&:hover .ckyc-card__cta {
background: $teal;
border-color: $teal;
@@ -420,9 +449,11 @@ $purple-bg: #f8f5ff;
.ckyc-card__icon {
background: linear-gradient(135deg, $amber, #d4892f);
}
.ckyc-card__glow {
background: radial-gradient(circle, $amber, transparent 60%);
}
&:hover .ckyc-card__cta {
background: $amber;
border-color: $amber;
@@ -433,15 +464,16 @@ $purple-bg: #f8f5ff;
.ckyc-card__icon {
background: linear-gradient(135deg, $purple, #8a6fd4);
}
.ckyc-card__glow {
background: radial-gradient(circle, $purple, transparent 60%);
}
&:hover .ckyc-card__cta {
background: $purple;
border-color: $purple;
}
}
}
.ckyc-tag {
@@ -458,12 +490,26 @@ $purple-bg: #f8f5ff;
/* ---------- Responsive ---------- */
@media (width <= 900px) {
.ckyc-dash { padding: 16px 18px 30px; }
.ckyc-dash__hero { flex-direction: column; align-items: flex-start; }
.ckyc-pipeline__track { flex-direction: row; flex-wrap: nowrap; }
.ckyc-dash {
padding: 16px 18px 30px;
}
.ckyc-dash__hero {
flex-direction: column;
align-items: flex-start;
}
.ckyc-pipeline__track {
flex-flow: row nowrap;
}
}
@media (width <= 600px) {
.ckyc-dash__grid { grid-template-columns: 1fr; }
.ckyc-phase { min-width: 120px; }
.ckyc-dash__grid {
grid-template-columns: 1fr;
}
.ckyc-phase {
min-width: 120px;
}
}
@@ -28,12 +28,7 @@
<!-- 3-tab filter -->
<div class="rm-tabs">
<button
type="button"
class="rm-tab"
[class.rm-tab--active]="activeTab === 'all'"
(click)="setTab('all')"
>
<button type="button" class="rm-tab" [class.rm-tab--active]="activeTab === 'all'" (click)="setTab('all')">
<fa-icon icon="list"></fa-icon>
<span>All clients</span>
<span class="rm-tab__count">{{ tabCounts.all }}</span>
@@ -75,12 +70,7 @@
<div class="rm-field">
<label class="rm-label"><fa-icon icon="cube"></fa-icon> Loan product ID</label>
<input
type="number"
class="rm-input"
formControlName="productId"
placeholder="Optional"
/>
<input type="number" class="rm-input" formControlName="productId" placeholder="Optional" />
</div>
<div class="rm-field">
@@ -186,9 +176,7 @@
<div class="rm-row__avatar">{{ row.fullName?.charAt(0) || '?' }}</div>
<div class="rm-row__ident">
<div class="rm-row__name">{{ row.fullName }}</div>
<div class="rm-row__account">
<fa-icon icon="hashtag"></fa-icon> {{ row.accountNo }}
</div>
<div class="rm-row__account"><fa-icon icon="hashtag"></fa-icon> {{ row.accountNo }}</div>
</div>
</div>
</td>
@@ -208,16 +196,36 @@
</td>
<td class="rm-table__artefacts">
<div class="rm-row__chips">
<span class="rm-chip" [class.rm-chip--ok]="row.hasPhoto" [class.rm-chip--miss]="!row.hasPhoto" title="Photo">
<span
class="rm-chip"
[class.rm-chip--ok]="row.hasPhoto"
[class.rm-chip--miss]="!row.hasPhoto"
title="Photo"
>
<fa-icon [icon]="row.hasPhoto ? 'check' : 'times'"></fa-icon> P
</span>
<span class="rm-chip" [class.rm-chip--ok]="row.hasSignature" [class.rm-chip--miss]="!row.hasSignature" title="Signature">
<span
class="rm-chip"
[class.rm-chip--ok]="row.hasSignature"
[class.rm-chip--miss]="!row.hasSignature"
title="Signature"
>
<fa-icon [icon]="row.hasSignature ? 'check' : 'times'"></fa-icon> S
</span>
<span class="rm-chip" [class.rm-chip--ok]="row.hasIdProof" [class.rm-chip--miss]="!row.hasIdProof" title="ID Proof">
<span
class="rm-chip"
[class.rm-chip--ok]="row.hasIdProof"
[class.rm-chip--miss]="!row.hasIdProof"
title="ID Proof"
>
<fa-icon [icon]="row.hasIdProof ? 'check' : 'times'"></fa-icon> ID
</span>
<span class="rm-chip" [class.rm-chip--ok]="row.hasAddressProof" [class.rm-chip--miss]="!row.hasAddressProof" title="Address Proof">
<span
class="rm-chip"
[class.rm-chip--ok]="row.hasAddressProof"
[class.rm-chip--miss]="!row.hasAddressProof"
title="Address Proof"
>
<fa-icon [icon]="row.hasAddressProof ? 'check' : 'times'"></fa-icon> Ad
</span>
</div>
@@ -233,11 +241,7 @@
</div>
</td>
<td class="rm-table__actions">
<button
class="rm-action-btn"
(click)="openView(row, $event)"
title="View details"
>
<button class="rm-action-btn" (click)="openView(row, $event)" title="View details">
<fa-icon icon="eye"></fa-icon>
</button>
<button
@@ -366,9 +370,7 @@
}
<!-- KYC snapshot -->
<div class="rm-view__section-title">
<fa-icon icon="shield-alt"></fa-icon> KYC Snapshot
</div>
<div class="rm-view__section-title"><fa-icon icon="shield-alt"></fa-icon> KYC Snapshot</div>
<div class="rm-view__kpis">
<div class="rm-kpi">
<div class="rm-kpi__label"><fa-icon icon="check-double"></fa-icon> KYC Status</div>
@@ -376,8 +378,14 @@
<div
class="rm-kpi__value rm-kpi__pill"
[class.rm-kpi__pill--ok]="viewDetails.kycStatusCode === 'clientKycStatus.verified'"
[class.rm-kpi__pill--warn]="viewDetails.kycStatusCode === 'clientKycStatus.pending' || viewDetails.kycStatusCode === 'clientKycStatus.initiated'"
[class.rm-kpi__pill--err]="viewDetails.kycStatusCode === 'clientKycStatus.rejected' || viewDetails.kycStatusCode === 'clientKycStatus.expired'"
[class.rm-kpi__pill--warn]="
viewDetails.kycStatusCode === 'clientKycStatus.pending' ||
viewDetails.kycStatusCode === 'clientKycStatus.initiated'
"
[class.rm-kpi__pill--err]="
viewDetails.kycStatusCode === 'clientKycStatus.rejected' ||
viewDetails.kycStatusCode === 'clientKycStatus.expired'
"
>
{{ viewDetails.kycStatus }}
</div>
@@ -409,14 +417,32 @@
</div>
<!-- 4 documents -->
<div class="rm-view__section-title">
<fa-icon icon="folder-open"></fa-icon> CKYC Documents
</div>
<div class="rm-view__section-title"><fa-icon icon="folder-open"></fa-icon> CKYC Documents</div>
<div class="rm-view__docs">
<ng-container *ngTemplateOutlet="docTile; context: { slot: viewDetails.docs.photo, label: 'Photo / Image of Client', icon: 'user-circle' }"></ng-container>
<ng-container *ngTemplateOutlet="docTile; context: { slot: viewDetails.docs.signature, label: 'Signature of Client', icon: 'file-signature' }"></ng-container>
<ng-container *ngTemplateOutlet="docTile; context: { slot: viewDetails.docs.idProof, label: 'Identity Proof', icon: 'id-card' }"></ng-container>
<ng-container *ngTemplateOutlet="docTile; context: { slot: viewDetails.docs.addressProof, label: 'Address Proof', icon: 'home' }"></ng-container>
<ng-container
*ngTemplateOutlet="
docTile;
context: { slot: viewDetails.docs.photo, label: 'Photo / Image of Client', icon: 'user-circle' }
"
></ng-container>
<ng-container
*ngTemplateOutlet="
docTile;
context: { slot: viewDetails.docs.signature, label: 'Signature of Client', icon: 'file-signature' }
"
></ng-container>
<ng-container
*ngTemplateOutlet="
docTile;
context: { slot: viewDetails.docs.idProof, label: 'Identity Proof', icon: 'id-card' }
"
></ng-container>
<ng-container
*ngTemplateOutlet="
docTile;
context: { slot: viewDetails.docs.addressProof, label: 'Address Proof', icon: 'home' }
"
></ng-container>
</div>
<ng-template #docTile let-slot="slot" let-label="label" let-icon="icon">
@@ -432,10 +458,7 @@
}
</div>
</div>
<fa-icon
class="rm-doc__state"
[icon]="slot.present ? 'check-circle' : 'times-circle'"
></fa-icon>
<fa-icon class="rm-doc__state" [icon]="slot.present ? 'check-circle' : 'times-circle'"></fa-icon>
</div>
@if (slot.files?.length) {
@@ -475,9 +498,7 @@
</div>
<div class="rm-modal__footer">
<button class="rm-btn rm-btn--ghost" (click)="closeView()">
<fa-icon icon="times"></fa-icon> Close
</button>
<button class="rm-btn rm-btn--ghost" (click)="closeView()"><fa-icon icon="times"></fa-icon> Close</button>
<button class="rm-btn rm-btn--primary" (click)="collectCkycFromView()">
<fa-icon icon="cloud-upload-alt"></fa-icon>
<span>Collect CKYC</span>
@@ -554,9 +575,7 @@
}
</div>
<div class="rm-step__content">
<div class="rm-step__label">
<fa-icon [icon]="phase.icon"></fa-icon> {{ phase.label }}
</div>
<div class="rm-step__label"><fa-icon [icon]="phase.icon"></fa-icon> {{ phase.label }}</div>
@if (phase.message) {
<div class="rm-step__msg">{{ phase.message }}</div>
}
@@ -677,7 +696,10 @@
class="rm-manifest__chip"
[class.rm-manifest__chip--ok]="entry.idProof?.packaged"
[class.rm-manifest__chip--miss]="!entry.idProof?.packaged"
[title]="(entry.idProof?.type || 'Identity Proof') + (entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '')"
[title]="
(entry.idProof?.type || 'Identity Proof') +
(entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '')
"
>
<fa-icon [icon]="entry.idProof?.packaged ? 'check' : 'times'"></fa-icon>
</span>
@@ -687,13 +709,23 @@
class="rm-manifest__chip"
[class.rm-manifest__chip--ok]="entry.addressProof?.packaged"
[class.rm-manifest__chip--miss]="!entry.addressProof?.packaged"
[title]="(entry.addressProof?.type || 'Address Proof') + (entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '')"
[title]="
(entry.addressProof?.type || 'Address Proof') +
(entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '')
"
>
<fa-icon [icon]="entry.addressProof?.packaged ? 'check' : 'times'"></fa-icon>
</span>
</td>
<td class="rm-manifest__td-right rm-manifest__size">
{{ formatBytes((entry.photo?.bytes || 0) + (entry.signature?.bytes || 0) + (entry.idProof?.bytes || 0) + (entry.addressProof?.bytes || 0)) }}
{{
formatBytes(
(entry.photo?.bytes || 0) +
(entry.signature?.bytes || 0) +
(entry.idProof?.bytes || 0) +
(entry.addressProof?.bytes || 0)
)
}}
</td>
</tr>
}
@@ -718,14 +750,14 @@
<div class="rm-modal__footer">
<button class="rm-btn rm-btn--ghost" [disabled]="zipRunning" (click)="closeZipDialog()">
@if (zipResult && !zipResult.error) { Close } @else { Cancel }
@if (zipResult && !zipResult.error) {
Close
} @else {
Cancel
}
</button>
@if (!zipResult || zipResult.error) {
<button
class="rm-btn rm-btn--amber"
[disabled]="zipRunning"
(click)="startZipCreation()"
>
<button class="rm-btn rm-btn--amber" [disabled]="zipRunning" (click)="startZipCreation()">
@if (zipRunning) {
<fa-icon icon="spinner" animation="spin"></fa-icon>
<span>Creating…</span>
@@ -762,9 +794,8 @@
<div class="rm-modal__body">
<p class="rm-modal__intro">
Transferring batch to the <strong>CERSAI SFTP</strong> drop server and waiting for
acknowledgement. On success, the returned <strong>CKYC numbers</strong> will be saved
against each client's master record.
Transferring batch to the <strong>CERSAI SFTP</strong> drop server and waiting for acknowledgement. On
success, the returned <strong>CKYC numbers</strong> will be saved against each client's master record.
</p>
@if (uploadResult?.status === 'ALREADY_UPLOADED') {
@@ -798,9 +829,7 @@
}
</div>
<div class="rm-step__content">
<div class="rm-step__label">
<fa-icon [icon]="phase.icon"></fa-icon> {{ phase.label }}
</div>
<div class="rm-step__label"><fa-icon [icon]="phase.icon"></fa-icon> {{ phase.label }}</div>
@if (phase.message) {
<div class="rm-step__msg">{{ phase.message }}</div>
}
@@ -824,7 +853,9 @@
<div class="rm-modal__result-grid">
<div>
<span class="rm-modal__result-label">CERSAI Reference</span>
<span class="rm-modal__result-value rm-modal__result-value--mono">{{ uploadResult.cersaiReference }}</span>
<span class="rm-modal__result-value rm-modal__result-value--mono">{{
uploadResult.cersaiReference
}}</span>
</div>
<div>
<span class="rm-modal__result-label">Batch ID</span>
@@ -860,11 +891,15 @@
</div>
<div class="rm-modal__result-grid-span">
<span class="rm-modal__result-label">Checksum sent</span>
<span class="rm-modal__result-value rm-modal__result-value--mono">{{ uploadResult.checksumSent }}</span>
<span class="rm-modal__result-value rm-modal__result-value--mono">{{
uploadResult.checksumSent
}}</span>
</div>
<div class="rm-modal__result-grid-span">
<span class="rm-modal__result-label">Checksum verified</span>
<span class="rm-modal__result-value rm-modal__result-value--mono">{{ uploadResult.checksumVerified }}</span>
<span class="rm-modal__result-value rm-modal__result-value--mono">{{
uploadResult.checksumVerified
}}</span>
</div>
</div>
</div>
@@ -918,14 +953,14 @@
<div class="rm-modal__footer">
<button class="rm-btn rm-btn--ghost" [disabled]="uploadRunning" (click)="closeUploadDialog()">
@if (uploadResult && !uploadResult.error) { Close } @else { Cancel }
@if (uploadResult && !uploadResult.error) {
Close
} @else {
Cancel
}
</button>
@if (!uploadResult || uploadResult.error) {
<button
class="rm-btn rm-btn--amber"
[disabled]="uploadRunning"
(click)="startSftpUpload()"
>
<button class="rm-btn rm-btn--amber" [disabled]="uploadRunning" (click)="startSftpUpload()">
@if (uploadRunning) {
<fa-icon icon="spinner" animation="spin"></fa-icon>
<span>Uploading…</span>
@@ -24,7 +24,9 @@ $amber: #ba7517;
$amber-light: #faeeda;
$teal-900: #085041;
.rm-container { padding: 20px 24px 40px; }
.rm-container {
padding: 20px 24px 40px;
}
.rm-header {
display: flex;
@@ -51,7 +53,7 @@ $teal-900: #085041;
align-items: center;
justify-content: center;
font-size: 18px;
box-shadow: 0 3px 10px rgba(26, 115, 232, 0.25);
box-shadow: 0 3px 10px rgb(26 115 232 / 25%);
flex-shrink: 0;
}
@@ -84,9 +86,14 @@ $teal-900: #085041;
align-items: center;
gap: 8px;
fa-icon { font-size: 13px; }
fa-icon {
font-size: 13px;
}
&--ghost { color: $gray-700 !important; border-color: $gray-200 !important; }
&--ghost {
color: $gray-700 !important;
border-color: $gray-200 !important;
}
&--primary {
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
@@ -102,7 +109,7 @@ $teal-900: #085041;
border: 1px solid $gray-200 !important;
border-radius: 14px !important;
padding: 8px !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) !important;
box-shadow: 0 1px 3px rgb(0 0 0 / 3%) !important;
}
button.rm-tab,
@@ -147,7 +154,9 @@ button.rm-tab,
background: $gray-50 !important;
color: $gray-900 !important;
fa-icon { color: $primary !important; }
fa-icon {
color: $primary !important;
}
}
}
@@ -175,7 +184,9 @@ button.rm-tab.rm-tab--active,
color: $primary-dark !important;
box-shadow: 0 2px 8px rgba($primary, 0.12) !important;
fa-icon { color: $primary !important; }
fa-icon {
color: $primary !important;
}
.rm-tab__count {
background: $primary !important;
@@ -190,7 +201,9 @@ button.rm-tab--amber.rm-tab--active,
color: #633806 !important;
box-shadow: 0 2px 8px rgba($amber, 0.18) !important;
fa-icon { color: $amber !important; }
fa-icon {
color: $amber !important;
}
.rm-tab__count {
background: $amber !important;
@@ -205,7 +218,9 @@ button.rm-tab--green.rm-tab--active,
color: $teal-900 !important;
box-shadow: 0 2px 8px rgba($green, 0.18) !important;
fa-icon { color: $green !important; }
fa-icon {
color: $green !important;
}
.rm-tab__count {
background: $green !important;
@@ -214,8 +229,13 @@ button.rm-tab--green.rm-tab--active,
}
@media (width <= 680px) {
.rm-tabs { flex-direction: column; }
.rm-tab { justify-content: space-between; }
.rm-tabs {
flex-direction: column;
}
.rm-tab {
justify-content: space-between;
}
}
/* Filters */
@@ -294,7 +314,10 @@ button.rm-tab--green.rm-tab--active,
align-items: center;
gap: 6px;
fa-icon { color: $primary; font-size: 11px; }
fa-icon {
color: $primary;
font-size: 11px;
}
}
.rm-input {
@@ -310,18 +333,20 @@ button.rm-tab--green.rm-tab--active,
box-sizing: border-box;
transition: all 0.15s;
&::placeholder { color: $gray-500; }
&::placeholder {
color: $gray-500;
}
&:focus {
outline: none;
border-color: $primary;
background: $white;
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
box-shadow: 0 0 0 3px rgb(26 115 232 / 10%);
}
&--sm {
width: auto;
padding: 6px 10px 6px 10px;
padding: 6px 10px;
font-size: 12px;
height: 30px;
}
@@ -330,8 +355,6 @@ button.rm-tab--green.rm-tab--active,
/* Native <select> — custom chevron, no overlap */
select.rm-input {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding-right: 34px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%235f6368' d='M0 0l5 6 5-6z'/></svg>");
background-repeat: no-repeat;
@@ -344,6 +367,7 @@ select.rm-input {
}
/* Native date input — cleaner calendar icon */
/* stylelint-disable-next-line selector-no-qualifying-type */
input[type='date'].rm-input {
padding-right: 10px;
@@ -354,11 +378,23 @@ input[type='date'].rm-input {
transition: opacity 0.15s;
}
&:hover::-webkit-calendar-picker-indicator { opacity: 0.9; }
&:hover::-webkit-calendar-picker-indicator {
opacity: 0.9;
}
&::-webkit-datetime-edit { color: $gray-900; padding-top: 1px; }
&::-webkit-datetime-edit-fields-wrapper { color: $gray-900; }
&::-webkit-datetime-edit-text { color: $gray-500; padding: 0 2px; }
&::-webkit-datetime-edit {
color: $gray-900;
padding-top: 1px;
}
&::-webkit-datetime-edit-fields-wrapper {
color: $gray-900;
}
&::-webkit-datetime-edit-text {
color: $gray-500;
padding: 0 2px;
}
&:not(:focus):placeholder-shown::-webkit-datetime-edit {
color: $gray-500;
@@ -381,7 +417,9 @@ input[type='date'].rm-input {
background: $primary-light;
border: 1px dashed rgba($primary, 0.3);
fa-icon { font-size: 20px; }
fa-icon {
font-size: 20px;
}
}
&--error {
@@ -389,7 +427,9 @@ input[type='date'].rm-input {
background: $red-light;
border: 1px solid rgba($red, 0.3);
fa-icon { font-size: 18px; }
fa-icon {
font-size: 18px;
}
}
&--empty {
@@ -397,7 +437,9 @@ input[type='date'].rm-input {
background: $gray-50;
border: 1px dashed $gray-200;
fa-icon { color: $green; }
fa-icon {
color: $green;
}
h3 {
margin: 4px 0 0;
@@ -406,7 +448,9 @@ input[type='date'].rm-input {
color: $gray-900;
}
p { margin: 0; }
p {
margin: 0;
}
}
}
@@ -433,9 +477,14 @@ input[type='date'].rm-input {
align-items: center;
gap: 6px;
fa-icon { color: $primary; }
fa-icon {
color: $primary;
}
strong { color: $gray-900; font-weight: 700; }
strong {
color: $gray-900;
font-weight: 700;
}
}
&__selected {
@@ -449,7 +498,9 @@ input[type='date'].rm-input {
border-radius: 14px;
border: 1px solid rgba($green, 0.3);
strong { font-weight: 700; }
strong {
font-weight: 700;
}
}
}
@@ -490,12 +541,15 @@ input[type='date'].rm-input {
vertical-align: middle;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:last-child td {
border-bottom: none;
}
&__check {
width: 42px;
text-align: center;
/* stylelint-disable-next-line selector-no-qualifying-type */
input[type='checkbox'] {
width: 16px;
height: 16px;
@@ -504,21 +558,35 @@ input[type='date'].rm-input {
}
}
&__artefacts { width: 180px; }
&__completion { width: 90px; text-align: center; }
&__actions { width: 90px; text-align: right; }
&__artefacts {
width: 180px;
}
&__completion {
width: 90px;
text-align: center;
}
&__actions {
width: 90px;
text-align: right;
}
}
.rm-row {
cursor: pointer;
transition: background 0.12s;
&:hover { background: #f5f8fc; }
&:hover {
background: #f5f8fc;
}
&--selected {
background: $primary-light !important;
td { border-bottom-color: rgba($primary, 0.15); }
td {
border-bottom-color: rgba($primary, 0.15);
}
}
&__client {
@@ -542,7 +610,9 @@ input[type='date'].rm-input {
flex-shrink: 0;
}
&__ident { min-width: 0; }
&__ident {
min-width: 0;
}
&__name {
font-size: 13px;
@@ -560,7 +630,9 @@ input[type='date'].rm-input {
font-family: 'SF Mono', Consolas, monospace;
margin-top: 1px;
fa-icon { font-size: 9px; }
fa-icon {
font-size: 9px;
}
}
&__line {
@@ -570,9 +642,14 @@ input[type='date'].rm-input {
font-size: 12px;
color: $gray-700;
+ .rm-row__line { margin-top: 3px; }
+ .rm-row__line {
margin-top: 3px;
}
fa-icon { color: $primary; font-size: 10px; }
fa-icon {
color: $primary;
font-size: 10px;
}
span {
white-space: nowrap;
@@ -630,10 +707,21 @@ input[type='date'].rm-input {
font-weight: 700;
border: 1px solid transparent;
fa-icon { font-size: 8px; }
fa-icon {
font-size: 8px;
}
&--ok { background: $green-light; color: $teal-900; border-color: rgba($green, 0.2); }
&--miss { background: $red-light; color: $red; border-color: rgba($red, 0.2); }
&--ok {
background: $green-light;
color: $teal-900;
border-color: rgba($green, 0.2);
}
&--miss {
background: $red-light;
color: $red;
border-color: rgba($red, 0.2);
}
}
.rm-action-btn {
@@ -647,7 +735,9 @@ input[type='date'].rm-input {
margin-left: 4px;
transition: all 0.12s;
fa-icon { font-size: 11px; }
fa-icon {
font-size: 11px;
}
&:hover {
background: $primary-light;
@@ -681,7 +771,7 @@ input[type='date'].rm-input {
background: $gray-900;
color: $white;
border-radius: 14px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
box-shadow: 0 12px 40px rgb(0 0 0 / 25%);
max-width: calc(100vw - 40px);
animation: rm-slide-up 0.25s ease-out;
@@ -697,8 +787,15 @@ input[type='date'].rm-input {
gap: 8px;
font-size: 13px;
fa-icon { color: #9fe1cb; }
strong { color: $white; font-weight: 700; font-size: 15px; }
fa-icon {
color: #9fe1cb;
}
strong {
color: $white;
font-weight: 700;
font-size: 15px;
}
}
&__warn {
@@ -712,7 +809,9 @@ input[type='date'].rm-input {
font-size: 11px;
font-weight: 600;
fa-icon { font-size: 10px; }
fa-icon {
font-size: 10px;
}
}
&__btns {
@@ -723,17 +822,24 @@ input[type='date'].rm-input {
}
@keyframes rm-slide-up {
from { transform: translate(-50%, 20px); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
from {
transform: translate(-50%, 20px);
opacity: 0;
}
to {
transform: translate(-50%, 0);
opacity: 1;
}
}
.rm-btn--ghost-light {
background: rgba(255, 255, 255, 0.1) !important;
background: rgb(255 255 255 / 10%) !important;
color: $white !important;
border-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgb(255 255 255 / 20%) !important;
&:hover {
background: rgba(255, 255, 255, 0.2) !important;
background: rgb(255 255 255 / 20%) !important;
}
}
@@ -742,14 +848,16 @@ input[type='date'].rm-input {
color: $white !important;
border: none !important;
&:hover { background: #a0651a !important; }
&:hover {
background: #a0651a !important;
}
}
/* ---------- Modal ---------- */
.rm-modal {
position: fixed;
inset: 0;
background: rgba(15, 25, 45, 0.55);
background: rgb(15 25 45 / 55%);
backdrop-filter: blur(3px);
z-index: 1200;
display: flex;
@@ -767,7 +875,7 @@ input[type='date'].rm-input {
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
box-shadow: 0 24px 60px rgb(0 0 0 / 25%);
animation: rm-zoom 0.25s ease-out;
}
@@ -817,7 +925,10 @@ input[type='date'].rm-input {
background: $red-light;
}
&:disabled { opacity: 0.4; cursor: not-allowed; }
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
&__body {
@@ -832,7 +943,10 @@ input[type='date'].rm-input {
line-height: 1.55;
margin: 0 0 16px;
strong { color: $gray-900; font-weight: 700; }
strong {
color: $gray-900;
font-weight: 700;
}
}
&__warn {
@@ -848,9 +962,18 @@ input[type='date'].rm-input {
color: #633806;
line-height: 1.5;
fa-icon { color: $amber; font-size: 14px; flex-shrink: 0; margin-top: 2px; }
fa-icon {
color: $amber;
font-size: 14px;
flex-shrink: 0;
margin-top: 2px;
}
strong { display: block; font-weight: 700; color: #4d2a05; }
strong {
display: block;
font-weight: 700;
color: #4d2a05;
}
}
&__result {
@@ -870,7 +993,10 @@ input[type='date'].rm-input {
color: $teal-900;
margin-bottom: 12px;
fa-icon { color: $green; font-size: 16px; }
fa-icon {
color: $green;
font-size: 16px;
}
}
&__result-grid {
@@ -920,7 +1046,12 @@ input[type='date'].rm-input {
font-size: 12px;
color: #5a1a0e;
fa-icon { color: $red; font-size: 14px; flex-shrink: 0; margin-top: 2px; }
fa-icon {
color: $red;
font-size: 14px;
flex-shrink: 0;
margin-top: 2px;
}
strong {
display: block;
@@ -953,7 +1084,9 @@ input[type='date'].rm-input {
text-align: left;
transition: background 0.15s;
&:hover { background: $gray-100; }
&:hover {
background: $gray-100;
}
> fa-icon:first-child {
color: $primary;
@@ -965,7 +1098,11 @@ input[type='date'].rm-input {
flex: 1;
min-width: 0;
strong { color: $gray-900; font-weight: 700; margin-right: 4px; }
strong {
color: $gray-900;
font-weight: 700;
margin-right: 4px;
}
}
}
@@ -986,7 +1123,9 @@ input[type='date'].rm-input {
text-transform: uppercase;
letter-spacing: 0.3px;
fa-icon { font-size: 9px; }
fa-icon {
font-size: 9px;
}
&--full {
background: $green-light;
@@ -1037,14 +1176,31 @@ input[type='date'].rm-input {
vertical-align: middle;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #f5f8fc; }
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: #f5f8fc;
}
}
&__th-center { text-align: center !important; width: 38px; }
&__th-right { text-align: right !important; }
&__td-center { text-align: center; }
&__td-right { text-align: right; }
&__th-center {
text-align: center !important;
width: 38px;
}
&__th-right {
text-align: right !important;
}
&__td-center {
text-align: center;
}
&__td-right {
text-align: right;
}
&__client {
display: flex;
@@ -1063,7 +1219,10 @@ input[type='date'].rm-input {
color: $gray-500;
font-family: 'SF Mono', Consolas, monospace;
fa-icon { font-size: 8px; margin-right: 2px; }
fa-icon {
font-size: 8px;
margin-right: 2px;
}
}
&__chip {
@@ -1165,7 +1324,10 @@ input[type='date'].rm-input {
align-items: center;
gap: 8px;
fa-icon { font-size: 12px; color: $gray-500; }
fa-icon {
font-size: 12px;
color: $gray-500;
}
}
&__msg {
@@ -1185,7 +1347,9 @@ input[type='date'].rm-input {
.rm-step__label {
color: $primary;
fa-icon { color: $primary; }
fa-icon {
color: $primary;
}
}
}
@@ -1196,12 +1360,16 @@ input[type='date'].rm-input {
color: $white;
}
&::after { background: $green; }
&::after {
background: $green;
}
.rm-step__label {
color: $teal-900;
fa-icon { color: $green; }
fa-icon {
color: $green;
}
}
}
@@ -1212,18 +1380,32 @@ input[type='date'].rm-input {
color: $white;
}
.rm-step__label { color: $red; }
.rm-step__label {
color: $red;
}
}
}
@keyframes rm-fade {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rm-zoom {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
/* ---------- View details modal ---------- */
@@ -1238,7 +1420,7 @@ input[type='date'].rm-input {
.rm-modal__title--teal fa-icon {
background: linear-gradient(135deg, #0f6e56, #1d9e75) !important;
box-shadow: 0 3px 10px rgba(29, 158, 117, 0.3) !important;
box-shadow: 0 3px 10px rgb(29 158 117 / 30%) !important;
}
.rm-modal__warn--blue {
@@ -1247,8 +1429,13 @@ input[type='date'].rm-input {
border-left-color: $primary !important;
color: #0c447c !important;
fa-icon { color: $primary !important; }
strong { color: #0c447c !important; }
fa-icon {
color: $primary !important;
}
strong {
color: #0c447c !important;
}
}
/* CKYC numbers table */
@@ -1272,7 +1459,10 @@ input[type='date'].rm-input {
gap: 6px;
border-bottom: 1px solid rgba($green, 0.2);
fa-icon { color: $green; font-size: 12px; }
fa-icon {
color: $green;
font-size: 12px;
}
}
&__table-wrap {
@@ -1308,7 +1498,9 @@ input[type='date'].rm-input {
color: $gray-900;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:last-child td {
border-bottom: none;
}
}
&__mono {
@@ -1323,7 +1515,10 @@ input[type='date'].rm-input {
background: rgba($green, 0.06);
}
&__status { text-align: right; white-space: nowrap; }
&__status {
text-align: right;
white-space: nowrap;
}
}
.rm-view {
@@ -1353,7 +1548,10 @@ input[type='date'].rm-input {
box-shadow: 0 3px 10px rgba($primary, 0.25);
}
&__client-info { min-width: 0; flex: 1; }
&__client-info {
min-width: 0;
flex: 1;
}
&__client-name {
font-size: 16px;
@@ -1370,7 +1568,11 @@ input[type='date'].rm-input {
flex-wrap: wrap;
gap: 6px;
fa-icon { color: $primary; font-size: 10px; margin-right: 3px; }
fa-icon {
color: $primary;
font-size: 10px;
margin-right: 3px;
}
}
&__cif {
@@ -1381,7 +1583,10 @@ input[type='date'].rm-input {
}
}
&__sep { color: $gray-200; margin: 0 2px; }
&__sep {
color: $gray-200;
margin: 0 2px;
}
&__loading {
padding: 10px 14px;
@@ -1394,7 +1599,9 @@ input[type='date'].rm-input {
align-items: center;
gap: 8px;
fa-icon { font-size: 14px; }
fa-icon {
font-size: 14px;
}
}
&__section-title {
@@ -1408,9 +1615,14 @@ input[type='date'].rm-input {
align-items: center;
gap: 6px;
fa-icon { color: $primary; font-size: 12px; }
fa-icon {
color: $primary;
font-size: 12px;
}
&:first-of-type { margin-top: 6px; }
&:first-of-type {
margin-top: 6px;
}
}
&__kpis {
@@ -1445,7 +1657,10 @@ input[type='date'].rm-input {
align-items: center;
gap: 5px;
fa-icon { color: $primary; font-size: 10px; }
fa-icon {
color: $primary;
font-size: 10px;
}
}
&__value {
@@ -1454,7 +1669,11 @@ input[type='date'].rm-input {
color: $gray-900;
}
&__missing { color: $gray-500; font-style: italic; font-weight: 500; }
&__missing {
color: $gray-500;
font-style: italic;
font-weight: 500;
}
&__pill {
display: inline-flex;
@@ -1515,7 +1734,10 @@ input[type='date'].rm-input {
flex-shrink: 0;
}
&__body { flex: 1; min-width: 0; }
&__body {
flex: 1;
min-width: 0;
}
&__label {
font-size: 10px;
@@ -1570,7 +1792,10 @@ input[type='date'].rm-input {
color: $gray-500;
font-style: italic;
fa-icon { color: $primary; font-size: 11px; }
fa-icon {
color: $primary;
font-size: 11px;
}
}
&--ok {
@@ -1582,8 +1807,13 @@ input[type='date'].rm-input {
color: $teal-900;
}
.rm-doc__state { color: $green; }
.rm-doc__files { border-top-color: rgba($green, 0.2); }
.rm-doc__state {
color: $green;
}
.rm-doc__files {
border-top-color: rgba($green, 0.2);
}
}
&--miss {
@@ -1595,7 +1825,9 @@ input[type='date'].rm-input {
color: $red;
}
.rm-doc__state { color: $red; }
.rm-doc__state {
color: $red;
}
}
}
@@ -1615,7 +1847,10 @@ input[type='date'].rm-input {
border-color: $primary;
box-shadow: 0 2px 6px rgba($primary, 0.1);
.rm-file__open { opacity: 1; color: $primary; }
.rm-file__open {
opacity: 1;
color: $primary;
}
}
&__thumb {
@@ -1707,7 +1942,7 @@ input[type='date'].rm-input {
&:hover {
border-color: $primary-light;
box-shadow: 0 4px 14px rgba(26, 115, 232, 0.1);
box-shadow: 0 4px 14px rgb(26 115 232 / 10%);
transform: translateY(-1px);
}
@@ -1756,7 +1991,10 @@ input[type='date'].rm-input {
font-family: 'SF Mono', Consolas, monospace;
margin-top: 2px;
fa-icon { font-size: 10px; margin-right: 2px; }
fa-icon {
font-size: 10px;
margin-right: 2px;
}
}
&__completion {
@@ -1835,7 +2073,11 @@ input[type='date'].rm-input {
color: $gray-700;
min-width: 0;
fa-icon { color: $primary; font-size: 11px; flex-shrink: 0; }
fa-icon {
color: $primary;
font-size: 11px;
flex-shrink: 0;
}
span {
white-space: nowrap;
@@ -1844,31 +2086,9 @@ input[type='date'].rm-input {
flex: 1;
}
&--missing { color: $gray-500; font-style: italic; }
}
.rm-chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 9px;
border-radius: 14px;
font-size: 11px;
font-weight: 600;
border: 1px solid transparent;
fa-icon { font-size: 10px; }
&--ok {
background: $green-light;
color: $teal-900;
border-color: rgba($green, 0.2);
}
&--miss {
background: $red-light;
color: $red;
border-color: rgba($red, 0.2);
&--missing {
color: $gray-500;
font-style: italic;
}
}
@@ -1910,13 +2130,28 @@ input[type='date'].rm-input {
border-radius: 8px !important;
color: $gray-700 !important;
&:disabled { color: $gray-200 !important; }
&:disabled {
color: $gray-200 !important;
}
}
}
@media (width <= 640px) {
.rm-container { padding: 14px; }
.rm-header { flex-direction: column; align-items: flex-start; }
.rm-cards { grid-template-columns: 1fr; }
.rm-pager { flex-direction: column; gap: 12px; }
.rm-container {
padding: 14px;
}
.rm-header {
flex-direction: column;
align-items: flex-start;
}
.rm-cards {
grid-template-columns: 1fr;
}
.rm-pager {
flex-direction: column;
gap: 12px;
}
}
@@ -221,12 +221,8 @@ export class RecordMasterComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (res: any) => {
(res?.idProofs || []).forEach((p: any) =>
this.proofKindMap.set(String(p.id), 'id')
);
(res?.addressProofs || []).forEach((p: any) =>
this.proofKindMap.set(String(p.id), 'address')
);
(res?.idProofs || []).forEach((p: any) => this.proofKindMap.set(String(p.id), 'id'));
(res?.addressProofs || []).forEach((p: any) => this.proofKindMap.set(String(p.id), 'address'));
this.proofKindLoaded = true;
// If records are already loaded, re-verify them now
if (this.rows.length > 0) this.reverifyArtefacts();
@@ -256,9 +252,7 @@ export class RecordMasterComponent implements OnInit {
const docTypeId = String(idf?.documentType?.id ?? '');
const kind = this.proofKindMap.get(docTypeId);
const isActive =
idf?.status === 'clientIdentifierStatusType.active' ||
idf?.status === 'Active' ||
!idf?.status;
idf?.status === 'clientIdentifierStatusType.active' || idf?.status === 'Active' || !idf?.status;
if (!isActive || !kind) continue;
if (kind === 'id') hasId = true;
else if (kind === 'address') hasAddr = true;
@@ -326,9 +320,7 @@ export class RecordMasterComponent implements OnInit {
/** Apply the tab's client-side filter and recompute counts */
private applyTabFilter(): void {
if (this.activeTab === 'ready') {
this.rows = this.allRows.filter(
(r) => r.hasPhoto && r.hasSignature && r.hasIdProof && r.hasAddressProof
);
this.rows = this.allRows.filter((r) => r.hasPhoto && r.hasSignature && r.hasIdProof && r.hasAddressProof);
} else {
this.rows = this.allRows;
}
@@ -338,12 +330,9 @@ export class RecordMasterComponent implements OnInit {
private computeCounts(all: CkycRecordRow[]) {
return {
all: all.length,
pendingCollection: all.filter(
(r) => !r.hasPhoto || !r.hasSignature || !r.hasIdProof || !r.hasAddressProof
).length,
ready: all.filter(
(r) => r.hasPhoto && r.hasSignature && r.hasIdProof && r.hasAddressProof
).length
pendingCollection: all.filter((r) => !r.hasPhoto || !r.hasSignature || !r.hasIdProof || !r.hasAddressProof)
.length,
ready: all.filter((r) => r.hasPhoto && r.hasSignature && r.hasIdProof && r.hasAddressProof).length
};
}
@@ -390,7 +379,11 @@ export class RecordMasterComponent implements OnInit {
}
openClient(row: CkycRecordRow): void {
this.router.navigate(['/clients/', row.clientId, 'general']);
this.router.navigate([
'/clients/',
row.clientId,
'general'
]);
}
openView(row: CkycRecordRow, event?: Event): void {
@@ -402,9 +395,17 @@ export class RecordMasterComponent implements OnInit {
row,
docs: {
photo: { present: row.hasPhoto, name: row.hasPhoto ? 'Profile photo' : 'Not uploaded', files: [] },
signature: { present: row.hasSignature, name: row.hasSignature ? 'Client signature' : 'Not uploaded', files: [] },
signature: {
present: row.hasSignature,
name: row.hasSignature ? 'Client signature' : 'Not uploaded',
files: []
},
idProof: { present: row.hasIdProof, name: row.hasIdProof ? 'Identifier' : 'Not uploaded', files: [] },
addressProof: { present: row.hasAddressProof, name: row.hasAddressProof ? 'Identifier' : 'Not uploaded', files: [] }
addressProof: {
present: row.hasAddressProof,
name: row.hasAddressProof ? 'Identifier' : 'Not uploaded',
files: []
}
}
};
@@ -486,9 +487,7 @@ export class RecordMasterComponent implements OnInit {
const docTypeId = String(idf?.documentType?.id ?? '');
const kind = this.proofKindMap.get(docTypeId);
const isActive =
idf?.status === 'clientIdentifierStatusType.active' ||
idf?.status === 'Active' ||
!idf?.status;
idf?.status === 'clientIdentifierStatusType.active' || idf?.status === 'Active' || !idf?.status;
if (!isActive || !kind) continue;
const proofName = idf?.documentType?.name || 'Identifier';
const rawKey = idf?.documentKey || '';
@@ -580,7 +579,11 @@ export class RecordMasterComponent implements OnInit {
private formatJavaDate(value: any): string {
if (!value) return '';
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d] = value;
const [
y,
m,
d
] = value;
return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`;
}
if (typeof value === 'string') return value;
@@ -608,7 +611,11 @@ export class RecordMasterComponent implements OnInit {
collectCkyc(row: CkycRecordRow, event: Event): void {
event.stopPropagation();
this.router.navigate(['/clients/', row.clientId, 'ckyc']);
this.router.navigate([
'/clients/',
row.clientId,
'ckyc'
]);
}
createClient(): void {
@@ -616,7 +623,12 @@ export class RecordMasterComponent implements OnInit {
}
artefactCompletion(row: CkycRecordRow): number {
const items = [row.hasPhoto, row.hasSignature, row.hasIdProof, row.hasAddressProof];
const items = [
row.hasPhoto,
row.hasSignature,
row.hasIdProof,
row.hasAddressProof
];
return items.filter(Boolean).length;
}
@@ -670,7 +682,11 @@ export class RecordMasterComponent implements OnInit {
this.zipRunning = false;
this.zipResult = null;
this.manifestExpanded = false;
this.zipPhases = this.zipPhases.map((p) => ({ ...p, status: 'pending' as const, message: undefined as string | undefined }));
this.zipPhases = this.zipPhases.map((p) => ({
...p,
status: 'pending' as const,
message: undefined as string | undefined
}));
}
closeZipDialog(): void {
@@ -683,7 +699,11 @@ export class RecordMasterComponent implements OnInit {
this.zipRunning = true;
this.zipResult = null;
// Reset phases
this.zipPhases = this.zipPhases.map((p) => ({ ...p, status: 'pending' as const, message: undefined as string | undefined }));
this.zipPhases = this.zipPhases.map((p) => ({
...p,
status: 'pending' as const,
message: undefined as string | undefined
}));
// Kick off visual phase animation for steps 1-4 while the POST is in flight.
// Step 5 (Register batch) is held until the API responds.
@@ -745,15 +765,21 @@ export class RecordMasterComponent implements OnInit {
// Don't override a phase that's already done/failed by the API response
if (this.zipPhases[i].status === 'pending') this.zipPhases[i].status = 'active';
}, i * step);
setTimeout(() => {
if (this.zipPhases[i].status === 'active') this.zipPhases[i].status = 'done';
}, (i + 1) * step);
setTimeout(
() => {
if (this.zipPhases[i].status === 'active') this.zipPhases[i].status = 'done';
},
(i + 1) * step
);
}
// Activate phase 5 after phases 1-4 finish, leave it active until API returns
setTimeout(() => {
const last = this.zipPhases.length - 1;
if (this.zipPhases[last].status === 'pending') this.zipPhases[last].status = 'active';
}, (this.zipPhases.length - 1) * step);
setTimeout(
() => {
const last = this.zipPhases.length - 1;
if (this.zipPhases[last].status === 'pending') this.zipPhases[last].status = 'active';
},
(this.zipPhases.length - 1) * step
);
}
proceedToUpload(): void {
@@ -840,7 +866,14 @@ export class RecordMasterComponent implements OnInit {
if (value == null || value === '') return '';
const pad = (n: any) => String(n ?? 0).padStart(2, '0');
if (Array.isArray(value) && value.length >= 3) {
const [y, mo, d, h = 0, mi = 0, s = 0] = value;
const [
y,
mo,
d,
h = 0,
mi = 0,
s = 0
] = value;
const date = `${pad(d)}-${pad(mo)}-${y}`;
if (value.length >= 4) {
return `${date} ${pad(h)}:${pad(mi)}:${pad(s)}`;
@@ -863,14 +896,20 @@ export class RecordMasterComponent implements OnInit {
setTimeout(() => {
if (this.uploadPhases[i].status === 'pending') this.uploadPhases[i].status = 'active';
}, i * step);
setTimeout(() => {
if (this.uploadPhases[i].status === 'active') this.uploadPhases[i].status = 'done';
}, (i + 1) * step);
setTimeout(
() => {
if (this.uploadPhases[i].status === 'active') this.uploadPhases[i].status = 'done';
},
(i + 1) * step
);
}
setTimeout(() => {
const last = this.uploadPhases.length - 1;
if (this.uploadPhases[last].status === 'pending') this.uploadPhases[last].status = 'active';
}, (this.uploadPhases.length - 1) * step);
setTimeout(
() => {
const last = this.uploadPhases.length - 1;
if (this.uploadPhases[last].status === 'pending') this.uploadPhases[last].status = 'active';
},
(this.uploadPhases.length - 1) * step
);
}
goToVerification(): void {