- Clients list: CKYC icon, CKYC Number column, status/legal form/CKYC filters - CKYC: SFTP upload logs, date formatting, modal z-index fix, non-dismissible dialogs - KYC Management: new standalone module with alert dashboard, notification scheduler, audit inventory, account status - Image cropper: ngx-image-cropper integration for front+back document merge into single PNG - Sidebar: KYC Management menu entry after CKYC Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
507 lines
20 KiB
HTML
507 lines
20 KiB
HTML
<!--
|
|
Copyright since 2025 Mifos Initiative
|
|
|
|
This Source Code Form is subject to the terms of the Mozilla Public
|
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
-->
|
|
|
|
<div class="fd-dialog" [class.fd-dialog--kyc]="documentIdentifier">
|
|
<div class="fd-header">
|
|
<h1 mat-dialog-title class="fd-title">
|
|
@if (documentIdentifier) {
|
|
<i class="fa fa-id-card fd-title__icon"></i>
|
|
{{ 'labels.heading.Add Client Identifier' | translate }}
|
|
} @else {
|
|
<i class="fa fa-upload fd-title__icon"></i>
|
|
{{ 'labels.heading.Upload' | translate }} {{ entityType }} {{ 'labels.heading.Documents' | translate }}
|
|
}
|
|
</h1>
|
|
<button mat-icon-button mat-dialog-close class="fd-close" aria-label="Close">
|
|
<i class="fa fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="fd-content">
|
|
@if (documentIdentifier) {
|
|
<div class="kyc-intro">
|
|
<i class="fa fa-shield kyc-intro__icon"></i>
|
|
<div>
|
|
<strong>KYC Document Upload</strong>
|
|
<p>
|
|
Upload KYC documents as per RBI guidelines. Accepted:
|
|
<i class="fa fa-file-image-o"></i> JPG,
|
|
<i class="fa fa-file-image-o"></i> PNG,
|
|
<i class="fa fa-file-pdf-o"></i> PDF — max 5MB each.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (loadingProofs) {
|
|
<div class="kyc-loading">
|
|
<i class="fa fa-spinner fa-spin"></i> Loading document types…
|
|
</div>
|
|
}
|
|
|
|
<form [formGroup]="uploadDocumentForm" class="kyc-form">
|
|
<!-- SECTION 1: IDENTITY PROOF -->
|
|
<div class="kyc-section" formGroupName="id">
|
|
<div class="kyc-section__header">
|
|
<div class="kyc-badge kyc-badge--blue">
|
|
<i class="fa fa-user-circle-o"></i>
|
|
</div>
|
|
<div class="kyc-section__titles">
|
|
<p class="kyc-section__title">
|
|
<i class="fa fa-id-badge"></i> Identity proof
|
|
</p>
|
|
<p class="kyc-section__sub">
|
|
Select document type — upload requirements will update automatically
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prefilled name (from client registration) -->
|
|
@if (clientName) {
|
|
<div class="kyc-prefill">
|
|
<i class="fa fa-user"></i>
|
|
<div>
|
|
<span class="kyc-prefill__label">Full name (from registration)</span>
|
|
<span class="kyc-prefill__value">{{ clientName }}</span>
|
|
</div>
|
|
<i class="fa fa-lock kyc-prefill__lock" title="Prefilled from client registration"></i>
|
|
</div>
|
|
}
|
|
|
|
<div class="kyc-field kyc-mb-sm">
|
|
<label class="kyc-label">
|
|
<i class="fa fa-file-text-o"></i> Document type
|
|
</label>
|
|
<div class="kyc-select-wrap">
|
|
<i class="fa fa-id-card kyc-select-wrap__icon"></i>
|
|
<select
|
|
class="kyc-input kyc-select"
|
|
formControlName="proofId"
|
|
(change)="onIdProofChange($any($event.target).value)"
|
|
>
|
|
<option value="">— Select document —</option>
|
|
@for (p of idProofs; track p.id) {
|
|
<option [value]="p.id">{{ p.name }}</option>
|
|
}
|
|
</select>
|
|
<i class="fa fa-chevron-down kyc-select-wrap__chevron"></i>
|
|
</div>
|
|
</div>
|
|
|
|
@if (selectedIdProof) {
|
|
<div class="kyc-field kyc-mb-sm">
|
|
<label class="kyc-label">
|
|
<i class="fa fa-hashtag"></i> {{ selectedIdProof.documentKeyLabel }}
|
|
</label>
|
|
<div class="kyc-input-wrap">
|
|
<i class="fa fa-pencil kyc-input-wrap__icon"></i>
|
|
<input
|
|
type="text"
|
|
class="kyc-input kyc-input--padded"
|
|
formControlName="documentKey"
|
|
[placeholder]="selectedIdProof.documentKeyPlaceholder || 'Enter document number'"
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (selectedIdProof?.info) {
|
|
<div class="kyc-banner kyc-banner--blue">
|
|
<i class="fa fa-info-circle"></i>
|
|
<span>{{ selectedIdProof.info }}</span>
|
|
</div>
|
|
}
|
|
|
|
@if (selectedIdProof) {
|
|
<div class="kyc-upload-grid" [class.kyc-upload-grid--two]="selectedIdProof.sides === 2">
|
|
<div class="kyc-field">
|
|
<div class="kyc-upload-label">
|
|
<span class="kyc-label">
|
|
<i class="fa fa-file-image-o"></i>
|
|
{{ selectedIdProof.sides === 2 ? 'Front side' : (selectedIdProof.name + ' (single copy)') }}
|
|
</span>
|
|
<span class="kyc-tag kyc-tag--req">
|
|
<i class="fa fa-asterisk"></i> Required
|
|
</span>
|
|
</div>
|
|
<label
|
|
class="kyc-dropzone"
|
|
[class.kyc-dropzone--has]="idFiles.front"
|
|
[class.kyc-dropzone--req]="!idFiles.front"
|
|
>
|
|
<input
|
|
type="file"
|
|
accept=".jpg,.jpeg,.png,.pdf"
|
|
hidden
|
|
(change)="onFilePick('id', 'front', $event)"
|
|
/>
|
|
@if (idFiles.front) {
|
|
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">File selected</p>
|
|
<p class="kyc-dropzone__name">
|
|
<i class="fa fa-paperclip"></i> {{ getFileName('id', 'front') }}
|
|
</p>
|
|
} @else {
|
|
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">Click to upload</p>
|
|
<p class="kyc-dropzone__hint">
|
|
<i class="fa fa-file-image-o"></i> JPG ·
|
|
<i class="fa fa-file-image-o"></i> PNG ·
|
|
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
|
</p>
|
|
}
|
|
</label>
|
|
</div>
|
|
|
|
@if (selectedIdProof.sides === 2) {
|
|
<div class="kyc-field">
|
|
<div class="kyc-upload-label">
|
|
<span class="kyc-label">
|
|
<i class="fa fa-file-image-o"></i> Back side
|
|
</span>
|
|
<span class="kyc-tag kyc-tag--req">
|
|
<i class="fa fa-asterisk"></i> Required
|
|
</span>
|
|
</div>
|
|
<label
|
|
class="kyc-dropzone"
|
|
[class.kyc-dropzone--has]="idFiles.back"
|
|
[class.kyc-dropzone--req]="!idFiles.back"
|
|
>
|
|
<input
|
|
type="file"
|
|
accept=".jpg,.jpeg,.png,.pdf"
|
|
hidden
|
|
(change)="onFilePick('id', 'back', $event)"
|
|
/>
|
|
@if (idFiles.back) {
|
|
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">File selected</p>
|
|
<p class="kyc-dropzone__name">
|
|
<i class="fa fa-paperclip"></i> {{ getFileName('id', 'back') }}
|
|
</p>
|
|
} @else {
|
|
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">Click to upload</p>
|
|
<p class="kyc-dropzone__hint">
|
|
<i class="fa fa-file-image-o"></i> JPG ·
|
|
<i class="fa fa-file-image-o"></i> PNG ·
|
|
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
|
</p>
|
|
}
|
|
</label>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (idMergedPreview && selectedIdProof?.sides === 2) {
|
|
<div class="kyc-merged">
|
|
<div class="kyc-merged__head">
|
|
<span class="kyc-merged__title">
|
|
<i class="fa fa-object-group"></i> Combined preview (front + back)
|
|
</span>
|
|
<span class="kyc-merged__hint">
|
|
Saved as a single PNG. Re-crop a side to update the merged file.
|
|
</span>
|
|
</div>
|
|
<div class="kyc-merged__preview">
|
|
<img [src]="idMergedPreview" alt="Merged ID document" />
|
|
</div>
|
|
<div class="kyc-merged__actions">
|
|
<button type="button" class="kyc-merged__btn" (click)="reCrop('id', 'front')">
|
|
<i class="fa fa-crop"></i> Re-crop front
|
|
</button>
|
|
<button type="button" class="kyc-merged__btn" (click)="reCrop('id', 'back')">
|
|
<i class="fa fa-crop"></i> Re-crop back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
<!-- SECTION 2: ADDRESS PROOF -->
|
|
<div class="kyc-section" formGroupName="address">
|
|
<div class="kyc-section__header">
|
|
<div class="kyc-badge kyc-badge--teal">
|
|
<i class="fa fa-map-marker"></i>
|
|
</div>
|
|
<div class="kyc-section__titles">
|
|
<p class="kyc-section__title">
|
|
<i class="fa fa-home"></i> Address proof
|
|
</p>
|
|
<p class="kyc-section__sub">
|
|
Select document type — upload requirements will update automatically
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prefilled permanent address -->
|
|
@if (clientAddress) {
|
|
<div class="kyc-prefill kyc-prefill--teal">
|
|
<i class="fa fa-map-marker"></i>
|
|
<div>
|
|
<span class="kyc-prefill__label">Permanent address (from registration)</span>
|
|
<span class="kyc-prefill__value">{{ clientAddress }}</span>
|
|
</div>
|
|
<i class="fa fa-lock kyc-prefill__lock" title="Prefilled from client registration"></i>
|
|
</div>
|
|
} @else {
|
|
<div class="kyc-banner kyc-banner--amber">
|
|
<i class="fa fa-exclamation-triangle"></i>
|
|
<span>No permanent address found on file. Add one in the Address tab first.</span>
|
|
</div>
|
|
}
|
|
|
|
<div class="kyc-field">
|
|
<label class="kyc-label">
|
|
<i class="fa fa-file-text-o"></i> Document type
|
|
</label>
|
|
<div class="kyc-chip-wrap">
|
|
@for (group of addressGroups; track group.label; let last = $last) {
|
|
<div class="kyc-chip-group-label">
|
|
<i class="fa fa-folder-open-o"></i> {{ group.label }}
|
|
</div>
|
|
<div class="kyc-chip-grid">
|
|
@for (p of group.items; track p.id) {
|
|
<span
|
|
class="kyc-chip"
|
|
[class.kyc-chip--sel]="isAddrProofSelected(p)"
|
|
(click)="selectAddrProof(p)"
|
|
(keydown.enter)="selectAddrProof(p)"
|
|
tabindex="0"
|
|
role="button"
|
|
>
|
|
@if (isAddrProofSelected(p)) {
|
|
<i class="fa fa-check-circle"></i>
|
|
} @else {
|
|
<i class="fa fa-circle-o"></i>
|
|
}
|
|
{{ p.name }}
|
|
</span>
|
|
}
|
|
</div>
|
|
@if (!last) {
|
|
<div class="kyc-chip-divider"></div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (selectedAddrProof?.info) {
|
|
<div class="kyc-banner kyc-banner--teal">
|
|
<i class="fa fa-check-circle"></i>
|
|
<span>{{ selectedAddrProof.info }}</span>
|
|
</div>
|
|
}
|
|
|
|
@if (selectedAddrProof) {
|
|
<div class="kyc-upload-grid" [class.kyc-upload-grid--two]="selectedAddrProof.sides === 2">
|
|
<div class="kyc-field">
|
|
<div class="kyc-upload-label">
|
|
<span class="kyc-label">
|
|
<i class="fa fa-file-image-o"></i>
|
|
{{ selectedAddrProof.sides === 2 ? 'Front side' : (selectedAddrProof.name + ' (single copy)') }}
|
|
</span>
|
|
<span class="kyc-tag kyc-tag--req">
|
|
<i class="fa fa-asterisk"></i> Required
|
|
</span>
|
|
</div>
|
|
<label
|
|
class="kyc-dropzone"
|
|
[class.kyc-dropzone--has]="addrFiles.front"
|
|
[class.kyc-dropzone--req]="!addrFiles.front"
|
|
>
|
|
<input
|
|
type="file"
|
|
accept=".jpg,.jpeg,.png,.pdf"
|
|
hidden
|
|
(change)="onFilePick('address', 'front', $event)"
|
|
/>
|
|
@if (addrFiles.front) {
|
|
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">File selected</p>
|
|
<p class="kyc-dropzone__name">
|
|
<i class="fa fa-paperclip"></i> {{ getFileName('address', 'front') }}
|
|
</p>
|
|
} @else {
|
|
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">Click to upload</p>
|
|
<p class="kyc-dropzone__hint">
|
|
<i class="fa fa-file-image-o"></i> JPG ·
|
|
<i class="fa fa-file-image-o"></i> PNG ·
|
|
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
|
</p>
|
|
}
|
|
</label>
|
|
</div>
|
|
|
|
@if (selectedAddrProof.sides === 2) {
|
|
<div class="kyc-field">
|
|
<div class="kyc-upload-label">
|
|
<span class="kyc-label">
|
|
<i class="fa fa-file-image-o"></i> Back side
|
|
</span>
|
|
<span class="kyc-tag kyc-tag--req">
|
|
<i class="fa fa-asterisk"></i> Required
|
|
</span>
|
|
</div>
|
|
<label
|
|
class="kyc-dropzone"
|
|
[class.kyc-dropzone--has]="addrFiles.back"
|
|
[class.kyc-dropzone--req]="!addrFiles.back"
|
|
>
|
|
<input
|
|
type="file"
|
|
accept=".jpg,.jpeg,.png,.pdf"
|
|
hidden
|
|
(change)="onFilePick('address', 'back', $event)"
|
|
/>
|
|
@if (addrFiles.back) {
|
|
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">File selected</p>
|
|
<p class="kyc-dropzone__name">
|
|
<i class="fa fa-paperclip"></i> {{ getFileName('address', 'back') }}
|
|
</p>
|
|
} @else {
|
|
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
|
<p class="kyc-dropzone__text">Click to upload</p>
|
|
<p class="kyc-dropzone__hint">
|
|
<i class="fa fa-file-image-o"></i> JPG ·
|
|
<i class="fa fa-file-image-o"></i> PNG ·
|
|
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
|
</p>
|
|
}
|
|
</label>
|
|
</div>
|
|
}
|
|
|
|
@if (selectedAddrProof.requiresDate) {
|
|
<div class="kyc-field kyc-field--date">
|
|
<label class="kyc-label">
|
|
<i class="fa fa-calendar"></i> Issue / statement date
|
|
<span class="kyc-opt">(not older than 3 months)</span>
|
|
</label>
|
|
<div class="kyc-input-wrap kyc-input-wrap--date">
|
|
<i class="fa fa-calendar-o kyc-input-wrap__icon"></i>
|
|
<input
|
|
type="date"
|
|
class="kyc-input kyc-input--date"
|
|
formControlName="issueDate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (addrMergedPreview && selectedAddrProof?.sides === 2) {
|
|
<div class="kyc-merged">
|
|
<div class="kyc-merged__head">
|
|
<span class="kyc-merged__title">
|
|
<i class="fa fa-object-group"></i> Combined preview (front + back)
|
|
</span>
|
|
<span class="kyc-merged__hint">
|
|
Saved as a single PNG. Re-crop a side to update the merged file.
|
|
</span>
|
|
</div>
|
|
<div class="kyc-merged__preview">
|
|
<img [src]="addrMergedPreview" alt="Merged address document" />
|
|
</div>
|
|
<div class="kyc-merged__actions">
|
|
<button type="button" class="kyc-merged__btn" (click)="reCrop('address', 'front')">
|
|
<i class="fa fa-crop"></i> Re-crop front
|
|
</button>
|
|
<button type="button" class="kyc-merged__btn" (click)="reCrop('address', 'back')">
|
|
<i class="fa fa-crop"></i> Re-crop back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
<div class="kyc-section kyc-section--notes">
|
|
<label class="kyc-label">
|
|
<i class="fa fa-sticky-note-o"></i> Description <span class="kyc-opt">(optional)</span>
|
|
</label>
|
|
<textarea
|
|
class="kyc-textarea"
|
|
formControlName="description"
|
|
placeholder="Add any additional notes about these documents…"
|
|
rows="2"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="kyc-status-bar">
|
|
<span>
|
|
<span class="kyc-dot" [class.kyc-dot--done]="isIdComplete()">
|
|
@if (isIdComplete()) { <i class="fa fa-check"></i> }
|
|
</span>
|
|
<i class="fa fa-id-badge"></i> Identity proof
|
|
</span>
|
|
<span>
|
|
<span class="kyc-dot" [class.kyc-dot--done]="isAddressComplete()">
|
|
@if (isAddressComplete()) { <i class="fa fa-check"></i> }
|
|
</span>
|
|
<i class="fa fa-home"></i> Address proof
|
|
</span>
|
|
<span class="kyc-status-hint">
|
|
<i class="fa fa-info-circle"></i> Fill in at least one section
|
|
</span>
|
|
</div>
|
|
</form>
|
|
} @else {
|
|
<form [formGroup]="uploadDocumentForm" class="fd-form">
|
|
<div class="fd-grid fd-grid--cols-2">
|
|
<div class="fd-field">
|
|
<mat-form-field class="flex-fill" appearance="outline">
|
|
<mat-label>{{ 'labels.inputs.File Name' | translate }}</mat-label>
|
|
<input formControlName="fileName" required matInput />
|
|
@if (uploadDocumentForm.controls.fileName.hasError('required')) {
|
|
<mat-error>
|
|
{{ 'labels.inputs.File Name' | translate }} {{ 'labels.commons.is' | translate }}
|
|
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
|
</mat-error>
|
|
}
|
|
</mat-form-field>
|
|
</div>
|
|
<div class="fd-field">
|
|
<mat-form-field class="flex-fill" appearance="outline">
|
|
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
|
|
<input formControlName="description" matInput />
|
|
</mat-form-field>
|
|
</div>
|
|
</div>
|
|
<div class="fd-field fd-field--file">
|
|
<mifosx-file-upload flex="100%" (change)="onFileSelect($event)"></mifosx-file-upload>
|
|
</div>
|
|
</form>
|
|
}
|
|
</div>
|
|
|
|
<mat-dialog-actions class="fd-actions" align="end">
|
|
<button mat-stroked-button mat-dialog-close class="fd-btn fd-btn--cancel">
|
|
<i class="fa fa-times fd-btn__icon"></i>
|
|
{{ 'labels.buttons.Cancel' | translate }}
|
|
</button>
|
|
<button
|
|
mat-flat-button
|
|
color="primary"
|
|
[disabled]="!canSubmit"
|
|
(click)="submit()"
|
|
class="fd-btn fd-btn--submit"
|
|
>
|
|
<i class="fa fa-check fd-btn__icon"></i>
|
|
@if (documentIdentifier) {
|
|
{{ 'labels.buttons.Submit' | translate }}
|
|
} @else {
|
|
{{ 'labels.buttons.Upload' | translate }}
|
|
}
|
|
</button>
|
|
</mat-dialog-actions>
|
|
</div>
|