feat: MFA/2FA, OTP audit logs, KYC + client/user enhancements
- Access Control module: SMS/Email/TOTP/Hardware-token MFA factor management - OTP Audit Logs module under Admin menu (list + detail) with 401/403 handling - Two-factor authentication login flow updates - Client KYC, India address, family-members, ckyc, nominee enhancements - New shared KYC helpers (kyc-proof-types, kyc-upload-helper.service) - User create/edit/view enhancements - Shell navigation: OTP Audit Logs entry under real Admin menu
This commit is contained in:
+157
-14
@@ -114,22 +114,165 @@
|
||||
|
||||
<div class="addr-divider"><span>Proof of Address</span></div>
|
||||
|
||||
<div class="addr-grid addr-grid--2">
|
||||
<mat-form-field appearance="outline" class="addr-ff">
|
||||
<mat-label>Address Proof Type</mat-label>
|
||||
<span matPrefix class="addr-prefix"><mat-icon>verified_user</mat-icon></span>
|
||||
<mat-select required formControlName="proofType">
|
||||
@for (pt of proofTypes; track pt) {
|
||||
<mat-option [value]="pt">{{ pt }}</mat-option>
|
||||
@if (loadingProofs) {
|
||||
<div class="addr-proof-loading">
|
||||
<fa-icon icon="spinner" [animation]="'spin'"></fa-icon>
|
||||
Loading document types…
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="addr-proof-field">
|
||||
<label class="addr-proof-label"><mat-icon>description</mat-icon> Document type</label>
|
||||
<div class="addr-chip-wrap">
|
||||
@for (group of addressGroups; track group.label; let last = $last) {
|
||||
<div class="addr-chip-group-label"><mat-icon>folder_open</mat-icon> {{ group.label }}</div>
|
||||
<div class="addr-chip-grid">
|
||||
@for (p of group.items; track p.id) {
|
||||
<span
|
||||
class="addr-chip"
|
||||
[class.addr-chip--sel]="isAddrProofSelected(p)"
|
||||
(click)="selectAddrProof(p)"
|
||||
(keydown.enter)="selectAddrProof(p)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
>
|
||||
<mat-icon class="addr-chip__icon">
|
||||
{{ isAddrProofSelected(p) ? 'check_circle' : 'radio_button_unchecked' }}
|
||||
</mat-icon>
|
||||
{{ p.name }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (!last) {
|
||||
<div class="addr-chip-divider"></div>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="addr-ff">
|
||||
<mat-label>Document Number</mat-label>
|
||||
<span matPrefix class="addr-prefix"><mat-icon>tag</mat-icon></span>
|
||||
<input matInput formControlName="proofDocNumber" placeholder="e.g. XXXX XXXX XXXX" />
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (selectedAddrProof?.info) {
|
||||
<div class="addr-proof-banner">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
<span>{{ selectedAddrProof.info }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedAddrProof) {
|
||||
<div class="addr-grid addr-grid--2">
|
||||
<mat-form-field appearance="outline" class="addr-ff">
|
||||
<mat-label>{{ selectedAddrProof.documentKeyLabel || 'Document number' }}</mat-label>
|
||||
<span matPrefix class="addr-prefix"><mat-icon>tag</mat-icon></span>
|
||||
<input
|
||||
matInput
|
||||
formControlName="documentKey"
|
||||
[placeholder]="selectedAddrProof.documentKeyPlaceholder || ''"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (selectedAddrProof.requiresDate) {
|
||||
<mat-form-field appearance="outline" class="addr-ff">
|
||||
<mat-label>Issue / bill date</mat-label>
|
||||
<span matPrefix class="addr-prefix"><mat-icon>event</mat-icon></span>
|
||||
<input matInput type="date" formControlName="issueDate" />
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="addr-upload-grid" [class.addr-upload-grid--two]="selectedAddrProof.sides === 2">
|
||||
<div class="addr-upload-slot">
|
||||
<div class="addr-upload-head">
|
||||
<span class="addr-upload-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
{{ selectedAddrProof.sides === 2 ? 'Front side' : selectedAddrProof.name + ' (single copy)' }}
|
||||
</span>
|
||||
<span class="addr-upload-req">Required</span>
|
||||
</div>
|
||||
<label
|
||||
class="addr-dropzone"
|
||||
[class.addr-dropzone--has]="addrFiles.front"
|
||||
[class.addr-dropzone--req]="!addrFiles.front"
|
||||
>
|
||||
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onAddrFile($event, 'front')" />
|
||||
@if (addrFiles.front) {
|
||||
<div class="addr-dropzone__file">
|
||||
<mat-icon>insert_drive_file</mat-icon>
|
||||
<span class="addr-dropzone__name">{{ addrFiles.front.name }}</span>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="addr-dropzone__remove"
|
||||
(click)="$event.preventDefault(); removeAddrFile('front')"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="addr-dropzone__hint">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
<span>Click to upload (JPG / PNG / PDF · max 5 MB)</span>
|
||||
</div>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (selectedAddrProof.sides === 2) {
|
||||
<div class="addr-upload-slot">
|
||||
<div class="addr-upload-head">
|
||||
<span class="addr-upload-title"> <mat-icon>image</mat-icon> Back side </span>
|
||||
<span class="addr-upload-req">Required</span>
|
||||
</div>
|
||||
<label
|
||||
class="addr-dropzone"
|
||||
[class.addr-dropzone--has]="addrFiles.back"
|
||||
[class.addr-dropzone--req]="!addrFiles.back"
|
||||
>
|
||||
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onAddrFile($event, 'back')" />
|
||||
@if (addrFiles.back) {
|
||||
<div class="addr-dropzone__file">
|
||||
<mat-icon>insert_drive_file</mat-icon>
|
||||
<span class="addr-dropzone__name">{{ addrFiles.back.name }}</span>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="addr-dropzone__remove"
|
||||
(click)="$event.preventDefault(); removeAddrFile('back')"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="addr-dropzone__hint">
|
||||
<mat-icon>cloud_upload</mat-icon>
|
||||
<span>Click to upload (JPG / PNG / PDF · max 5 MB)</span>
|
||||
</div>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (selectedAddrProof.sides === 2 && addrMergedPreview) {
|
||||
<div class="addr-merged">
|
||||
<div class="addr-merged__head">
|
||||
<span class="addr-merged__title">
|
||||
<mat-icon>filter_none</mat-icon>
|
||||
Combined preview (front + back)
|
||||
</span>
|
||||
<span class="addr-merged__hint">Saved as a single PNG. Re-crop a side to update the merged file.</span>
|
||||
</div>
|
||||
<div class="addr-merged__preview">
|
||||
<img [src]="addrMergedPreview" alt="Merged address proof" />
|
||||
</div>
|
||||
<div class="addr-merged__actions">
|
||||
<button type="button" mat-stroked-button class="addr-merged__btn" (click)="reCropAddrFile('front')">
|
||||
<mat-icon>crop</mat-icon> Re-crop front
|
||||
</button>
|
||||
<button type="button" mat-stroked-button class="addr-merged__btn" (click)="reCropAddrFile('back')">
|
||||
<mat-icon>crop</mat-icon> Re-crop back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
|
||||
+343
@@ -217,3 +217,346 @@ $muted: #64748b;
|
||||
color: #1565c0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Address-proof picker ===================== */
|
||||
.addr-proof-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.addr-proof-field {
|
||||
margin: 4px 0 18px;
|
||||
}
|
||||
|
||||
.addr-proof-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin-bottom: 10px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-chip-wrap {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.addr-chip-group-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.6px;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-chip-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.addr-chip-divider {
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
margin: 14px 0 10px;
|
||||
}
|
||||
|
||||
.addr-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s,
|
||||
color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1976d2;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&--sel {
|
||||
background: #e3f2fd;
|
||||
border-color: #1976d2;
|
||||
color: #0d47a1;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 1px #1976d2;
|
||||
|
||||
.addr-chip__icon {
|
||||
color: #1976d2;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #1976d2;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-proof-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
margin: 12px 0 16px;
|
||||
border-radius: 10px;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
font-size: 13px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #059669;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-upload-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
|
||||
&--two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (width <= 720px) {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-upload-slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.addr-upload-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.addr-upload-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-upload-req {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
border: 1px solid #fecaca;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.addr-merged {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 14px 16px;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 360px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 6px rgb(15 23 42 / 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addr-dropzone {
|
||||
display: block;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 18px 14px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
&--req {
|
||||
border-color: #fca5a5;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
&--has {
|
||||
border-style: solid;
|
||||
border-color: #86efac;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
mat-icon {
|
||||
color: #16a34a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
+124
-11
@@ -14,7 +14,10 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconButton, MatButton } from '@angular/material/button';
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
|
||||
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-client-india-address-step',
|
||||
@@ -25,13 +28,16 @@ import { ClientsService } from '../../clients.service';
|
||||
FaIconComponent,
|
||||
MatCheckbox,
|
||||
MatTabsModule,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatButton
|
||||
]
|
||||
})
|
||||
export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
private fb = inject(UntypedFormBuilder);
|
||||
private http = inject(HttpClient);
|
||||
private clientsService = inject(ClientsService);
|
||||
private kycUpload = inject(KycUploadHelperService);
|
||||
|
||||
/** Client ID — when set, loads existing addresses for edit mode */
|
||||
@Input() clientId: string = '';
|
||||
@@ -66,11 +72,12 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
],
|
||||
country: [{ value: 'India', disabled: true }],
|
||||
landmark: [''],
|
||||
proofType: [
|
||||
proofId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
proofDocNumber: ['']
|
||||
documentKey: [''],
|
||||
issueDate: ['']
|
||||
});
|
||||
|
||||
currentForm: UntypedFormGroup = this.fb.group({
|
||||
@@ -166,14 +173,15 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
correspondence: []
|
||||
};
|
||||
|
||||
proofTypes = [
|
||||
'Aadhaar Card',
|
||||
'Passport',
|
||||
'Voter ID',
|
||||
'Driving Licence',
|
||||
'Utility Bill (not older than 3 months)',
|
||||
'Bank Statement'
|
||||
];
|
||||
/** KYC address-proof picker state (mirrors identities-tab dialog) */
|
||||
addressProofs: ProofType[] = [];
|
||||
addressGroups: { label: string; items: ProofType[] }[] = [];
|
||||
selectedAddrProof: ProofType | null = null;
|
||||
loadingProofs = false;
|
||||
addrFiles: { front: File | null; back: File | null } = { front: null, back: null };
|
||||
/** Combined front+back PNG (built automatically once both image sides are ready) */
|
||||
addrMergedFile: File | null = null;
|
||||
addrMergedPreview: string | null = null;
|
||||
|
||||
residenceTypes = [
|
||||
'Owned',
|
||||
@@ -206,11 +214,116 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
|
||||
private typeToTab: Record<string, string> = {};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadProofTypes();
|
||||
if (this.clientId) {
|
||||
this.loadExistingAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
private loadProofTypes(): void {
|
||||
this.loadingProofs = true;
|
||||
this.clientsService.getKycProofTypes().subscribe({
|
||||
next: (res: any) => {
|
||||
const activeFilter = (p: any) => p?.active !== false;
|
||||
this.addressProofs = (res?.addressProofs || []).filter(activeFilter).map(normalizeProofType);
|
||||
this.addressGroups = groupProofsByCategory(this.addressProofs);
|
||||
this.loadingProofs = false;
|
||||
},
|
||||
error: () => {
|
||||
this.loadingProofs = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectAddrProof(proof: ProofType): void {
|
||||
this.selectedAddrProof = proof;
|
||||
this.permanentForm.get('proofId')?.setValue(proof.id);
|
||||
this.addrFiles = { front: null, back: null };
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
}
|
||||
|
||||
isAddrProofSelected(proof: ProofType): boolean {
|
||||
return this.selectedAddrProof?.id === proof.id;
|
||||
}
|
||||
|
||||
async onAddrFile(event: Event, slot: 'front' | 'back'): Promise<void> {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const raw = input.files?.[0];
|
||||
input.value = '';
|
||||
if (!raw) return;
|
||||
if (raw.size > 5 * 1024 * 1024) return;
|
||||
const cropped = await this.kycUpload.pickWithCrop(raw, `Crop ${slot} side`);
|
||||
if (!cropped) return;
|
||||
this.addrFiles[slot] = cropped;
|
||||
await this.refreshAddrMerged();
|
||||
}
|
||||
|
||||
async reCropAddrFile(slot: 'front' | 'back'): Promise<void> {
|
||||
const current = this.addrFiles[slot];
|
||||
if (!current || !current.type.startsWith('image/')) return;
|
||||
const cropped = await this.kycUpload.pickWithCrop(current, `Re-crop ${slot} side`);
|
||||
if (!cropped) return;
|
||||
this.addrFiles[slot] = cropped;
|
||||
await this.refreshAddrMerged();
|
||||
}
|
||||
|
||||
removeAddrFile(slot: 'front' | 'back'): void {
|
||||
this.addrFiles[slot] = null;
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
}
|
||||
|
||||
private async refreshAddrMerged(): Promise<void> {
|
||||
const proof = this.selectedAddrProof;
|
||||
if (!proof || proof.sides !== 2 || !this.addrFiles.front || !this.addrFiles.back) {
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
return;
|
||||
}
|
||||
const merged = await this.kycUpload.mergeFrontBack(
|
||||
this.addrFiles.front,
|
||||
this.addrFiles.back,
|
||||
proof.name || 'address-proof'
|
||||
);
|
||||
if (merged) {
|
||||
this.addrMergedFile = merged.file;
|
||||
this.addrMergedPreview = merged.preview;
|
||||
} else {
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Files + proof metadata for the parent to persist (e.g. via client identifier API).
|
||||
* When both image sides are present they're sent as a single combined PNG.
|
||||
*/
|
||||
get addressProofPayload(): {
|
||||
proof: ProofType | null;
|
||||
proofId: any;
|
||||
documentKey: string;
|
||||
issueDate: string;
|
||||
files: { slot: string; file: File }[];
|
||||
} {
|
||||
const files: { slot: string; file: File }[] = [];
|
||||
if (this.selectedAddrProof?.sides === 2 && this.addrMergedFile) {
|
||||
files.push({ slot: 'Combined', file: this.addrMergedFile });
|
||||
} else {
|
||||
if (this.addrFiles.front) files.push({ slot: 'Front', file: this.addrFiles.front });
|
||||
if (this.selectedAddrProof?.sides === 2 && this.addrFiles.back) {
|
||||
files.push({ slot: 'Back', file: this.addrFiles.back });
|
||||
}
|
||||
}
|
||||
return {
|
||||
proof: this.selectedAddrProof,
|
||||
proofId: this.permanentForm.get('proofId')?.value || '',
|
||||
documentKey: this.permanentForm.get('documentKey')?.value || '',
|
||||
issueDate: this.permanentForm.get('issueDate')?.value || '',
|
||||
files
|
||||
};
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['readOnly']) {
|
||||
const forms = [
|
||||
|
||||
Reference in New Issue
Block a user