@@ -305,23 +305,85 @@
|
||||
<mat-datepicker #incorpDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- GSTIN (all non-individual) -->
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>GSTIN Number</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="15-character GSTIN"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<!-- GSTIN (all non-individual) with Verify -->
|
||||
<div class="form-field verify-field">
|
||||
<mat-form-field appearance="outline" class="verify-input">
|
||||
<mat-label>GSTIN Number</mat-label>
|
||||
<span matPrefix class="field-prefix-icon"><mat-icon>receipt_long</mat-icon></span>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="15-character GSTIN"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'FAILED') {
|
||||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="verify-action">
|
||||
@if (gstinVerifyStatus === 'NONE' || gstinVerifyStatus === 'FAILED') {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="verify-btn-inline"
|
||||
[disabled]="
|
||||
readOnly ||
|
||||
!kycForm.get('gstinNumber')?.value ||
|
||||
kycForm.get('gstinNumber')?.value.length < 15 ||
|
||||
gstinVerifying ||
|
||||
!clientId
|
||||
"
|
||||
(click)="verifyGstin()"
|
||||
>
|
||||
@if (gstinVerifying) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<fa-icon icon="shield-alt"></fa-icon> Verify GSTIN
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<div class="pan-result-card full-width">
|
||||
<div class="pan-result-header">
|
||||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||||
<span>GSTIN Verified Successfully</span>
|
||||
</div>
|
||||
<div class="pan-result-body">
|
||||
@if (gstinVerifyName) {
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">Trade Name / Legal Name</span>
|
||||
<span class="pan-result-value name">{{ gstinVerifyName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (gstinVerifyState) {
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">State</span>
|
||||
<span class="pan-result-value">{{ gstinVerifyState }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">Verified On</span>
|
||||
<span class="pan-result-value date">{{ gstinVerifiedOn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PAN — Proprietorship uses Individual PAN, others use Enterprise PAN -->
|
||||
@if (constitutionMeta?.value === 'PROPRIETORSHIP') {
|
||||
|
||||
@@ -263,6 +263,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
entPanVerifyName = '';
|
||||
entPanVerifiedOn = '';
|
||||
|
||||
/** GSTIN verification */
|
||||
gstinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
gstinVerifying = false;
|
||||
gstinVerifyName = '';
|
||||
gstinVerifiedOn = '';
|
||||
gstinVerifyState = '';
|
||||
|
||||
/** CIN verification (dummy) */
|
||||
cinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
cinVerifying = false;
|
||||
@@ -480,6 +487,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (data.panAadhaarSeedingStatus) {
|
||||
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
|
||||
}
|
||||
if (data.gstinVerified) {
|
||||
this.gstinVerifyStatus = 'VERIFIED';
|
||||
this.gstinVerifyName = data.gstinTradeName || data.gstinLegalName || '';
|
||||
this.gstinVerifyState = data.gstinState || '';
|
||||
const rawGstinDate = data.gstinVerifiedOn;
|
||||
if (Array.isArray(rawGstinDate) && rawGstinDate.length >= 3) {
|
||||
this.gstinVerifiedOn = `${rawGstinDate[2]?.toString().padStart(2, '0')}/${rawGstinDate[1]?.toString().padStart(2, '0')}/${rawGstinDate[0]}`;
|
||||
} else if (rawGstinDate) {
|
||||
this.gstinVerifiedOn = rawGstinDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load existing photo & signature
|
||||
this.loadExistingPhoto();
|
||||
@@ -772,6 +790,71 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify GSTIN via API */
|
||||
verifyGstin() {
|
||||
if (!this.clientId) return;
|
||||
const gstin = this.kycForm.value.gstinNumber;
|
||||
if (!gstin || this.kycForm.get('gstinNumber')?.invalid) return;
|
||||
|
||||
this.gstinVerifying = true;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
gstinNumber: gstin,
|
||||
dateFormat,
|
||||
panNumber: this.kycForm.value.enterprisePan || this.kycForm.value.panNumber || null,
|
||||
aadhaarNumberMasked: 'XXXXXXXX0000'
|
||||
};
|
||||
|
||||
this.saveKycAndDo(
|
||||
payload,
|
||||
() => {
|
||||
this.clientsService.verifyGSTIN(this.clientId, this.kycId).subscribe({
|
||||
next: (res: any) => {
|
||||
this.gstinVerifying = false;
|
||||
const changes = res?.changes || res;
|
||||
if (changes?.gstinVerified === true) {
|
||||
this.gstinVerifyStatus = 'VERIFIED';
|
||||
this.gstinVerifyName = changes?.gstinTradeName || changes?.gstinLegalName || '';
|
||||
this.gstinVerifyState = changes?.gstinState || '';
|
||||
const rawDate = changes?.gstinVerifiedOn;
|
||||
if (Array.isArray(rawDate)) {
|
||||
this.gstinVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
|
||||
} else {
|
||||
const now = new Date();
|
||||
this.gstinVerifiedOn =
|
||||
rawDate ||
|
||||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||
}
|
||||
} else {
|
||||
this.gstinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
changes?.gstinVerificationMessage ||
|
||||
'GSTIN verification failed. Please check the GSTIN number and try again.'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.gstinVerifying = false;
|
||||
this.gstinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
'GSTIN verification failed.'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
(msg) => {
|
||||
this.gstinVerifying = false;
|
||||
this.showPanErrorAlert(msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify CIN (dummy — no real API) */
|
||||
verifyCin() {
|
||||
const cin = this.kycForm.value.cinNumber;
|
||||
|
||||
+12
-2
@@ -1,3 +1,11 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Copyright since 2025 Mifos Initiative
|
||||
-->
|
||||
@@ -20,11 +28,13 @@
|
||||
|
||||
<div class="crop-workspace" [class.hidden]="!imageLoaded">
|
||||
<div class="crop-source">
|
||||
<canvas #sourceCanvas
|
||||
<canvas
|
||||
#sourceCanvas
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(mousemove)="onMouseMove($event)"
|
||||
(mouseup)="onMouseUp()"
|
||||
(mouseleave)="onMouseUp()">
|
||||
(mouseleave)="onMouseUp()"
|
||||
>
|
||||
</canvas>
|
||||
<div class="crop-hint">Drag to move. Drag corner to resize.</div>
|
||||
</div>
|
||||
|
||||
+9
-1
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*/
|
||||
@@ -158,7 +166,7 @@ $navy: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
.crop-workspace {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
+55
-21
@@ -1,8 +1,12 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { Component, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import { Component, inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
@@ -59,10 +63,8 @@ export class ImageCropDialogComponent implements AfterViewInit {
|
||||
|
||||
zoom = 1;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ImageCropDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ImageCropData
|
||||
) {}
|
||||
dialogRef = inject(MatDialogRef<ImageCropDialogComponent>);
|
||||
data = inject<ImageCropData>(MAT_DIALOG_DATA);
|
||||
|
||||
ngAfterViewInit() {
|
||||
const reader = new FileReader();
|
||||
@@ -127,9 +129,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
|
||||
ctx.clearRect(this.cropX, this.cropY, this.cropW, this.cropH);
|
||||
ctx.drawImage(
|
||||
this.img,
|
||||
this.cropX * this.scaleX, this.cropY * this.scaleY,
|
||||
this.cropW * this.scaleX, this.cropH * this.scaleY,
|
||||
this.cropX, this.cropY, this.cropW, this.cropH
|
||||
this.cropX * this.scaleX,
|
||||
this.cropY * this.scaleY,
|
||||
this.cropW * this.scaleX,
|
||||
this.cropH * this.scaleY,
|
||||
this.cropX,
|
||||
this.cropY,
|
||||
this.cropW,
|
||||
this.cropH
|
||||
);
|
||||
|
||||
// Crop border
|
||||
@@ -141,14 +148,31 @@ export class ImageCropDialogComponent implements AfterViewInit {
|
||||
const hs = 8;
|
||||
ctx.fillStyle = '#1353a0';
|
||||
const corners = [
|
||||
[this.cropX, this.cropY],
|
||||
[this.cropX + this.cropW, this.cropY],
|
||||
[this.cropX, this.cropY + this.cropH],
|
||||
[this.cropX + this.cropW, this.cropY + this.cropH]
|
||||
[
|
||||
this.cropX,
|
||||
this.cropY
|
||||
],
|
||||
[
|
||||
this.cropX + this.cropW,
|
||||
this.cropY
|
||||
],
|
||||
[
|
||||
this.cropX,
|
||||
this.cropY + this.cropH
|
||||
],
|
||||
[
|
||||
this.cropX + this.cropW,
|
||||
this.cropY + this.cropH
|
||||
]
|
||||
];
|
||||
corners.forEach(([x, y]) => {
|
||||
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
|
||||
});
|
||||
corners.forEach(
|
||||
([
|
||||
x,
|
||||
y
|
||||
]) => {
|
||||
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private updatePreview() {
|
||||
@@ -163,9 +187,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
|
||||
pCtx.clearRect(0, 0, pw, ph);
|
||||
pCtx.drawImage(
|
||||
this.img,
|
||||
this.cropX * this.scaleX, this.cropY * this.scaleY,
|
||||
this.cropW * this.scaleX, this.cropH * this.scaleY,
|
||||
0, 0, pw, ph
|
||||
this.cropX * this.scaleX,
|
||||
this.cropY * this.scaleY,
|
||||
this.cropW * this.scaleX,
|
||||
this.cropH * this.scaleY,
|
||||
0,
|
||||
0,
|
||||
pw,
|
||||
ph
|
||||
);
|
||||
}
|
||||
|
||||
@@ -229,9 +258,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
|
||||
const ctx = offscreen.getContext('2d')!;
|
||||
ctx.drawImage(
|
||||
this.img,
|
||||
this.cropX * this.scaleX, this.cropY * this.scaleY,
|
||||
this.cropW * this.scaleX, this.cropH * this.scaleY,
|
||||
0, 0, offscreen.width, offscreen.height
|
||||
this.cropX * this.scaleX,
|
||||
this.cropY * this.scaleY,
|
||||
this.cropW * this.scaleX,
|
||||
this.cropH * this.scaleY,
|
||||
0,
|
||||
0,
|
||||
offscreen.width,
|
||||
offscreen.height
|
||||
);
|
||||
|
||||
offscreen.toBlob((blob) => {
|
||||
|
||||
Reference in New Issue
Block a user