From 79e455c4edb365b96d0ba59555352d330726bf33 Mon Sep 17 00:00:00 2001 From: Aditya Date: Tue, 7 Apr 2026 15:08:07 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20edit=20client=20=E2=80=94=20save=20per?= =?UTF-8?q?=20tab,=20DOB=20re-KYC=20alert,=20audit=20logs,=20KYC=20improve?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Edit button per tab with Save & Next confirmation dialog - DOB change triggers re-KYC alert, resets PAN verification, auto-fills remarks - Mobile/email changes logged as audit notes via client notes API - Redesigned confirmation dialog with icon + title, type-based theming - KYC re-edit flow: remarks mandatory for VERIFIED records, re-edit API command - Load existing photo & signature in edit KYC, immediate upload on change - Populate all KYC fields (document, AML, form60/15) from API on edit - Non-individual PAN verification uses same flow as individual - Fix table overflow when sidebar panel is open - Add fr unit to stylelint allowed-list Co-Authored-By: Claude Opus 4.6 (1M context) --- .stylelintrc | 3 +- .../client-kyc-step.component.html | 328 ++++++-- .../client-kyc-step.component.scss | 135 +++- .../client-kyc-step.component.ts | 710 +++++++++++++----- src/app/clients/clients.service.ts | 54 +- .../edit-client/edit-client.component.html | 40 +- .../edit-client/edit-client.component.scss | 39 +- .../edit-client/edit-client.component.ts | 332 +++++++- src/app/core/shell/shell.component.scss | 42 +- .../confirmation-dialog.component.html | 47 +- .../confirmation-dialog.component.scss | 142 ++++ .../confirmation-dialog.component.ts | 21 +- src/app/shared/styles/list-layout.scss | 192 +++-- src/environments/.env.ts | 12 +- 14 files changed, 1699 insertions(+), 398 deletions(-) diff --git a/.stylelintrc b/.stylelintrc index b720d1a7a..fa5dba7ef 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -67,7 +67,8 @@ "vh", "deg", "s", - "ms" + "ms", + "fr" ] } } diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html index ffa19fdda..daee1a515 100644 --- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html @@ -1,5 +1,9 @@
@@ -22,7 +26,6 @@
- @if (!isNonIndividual) {
@@ -35,9 +38,16 @@ PAN Number credit_card - + @if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) { Invalid PAN format } @@ -51,9 +61,16 @@
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') { -
PAN–Aadhaar Link Status - - @if (panAadhaarSeedingStatus?.toUpperCase() === 'Y') { Linked } - @else if (panAadhaarSeedingStatus?.toUpperCase() === 'N') { Not Linked } - @else { — } + + @if (panAadhaarSeedingStatus?.toUpperCase() === 'Y') { + Linked + } @else if (panAadhaarSeedingStatus?.toUpperCase() === 'N') { + Not Linked + } @else { + — + }
@@ -99,10 +124,22 @@ Aadhaar Number fingerprint - - @if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) { @@ -114,9 +151,19 @@
@if ((aadhaarVerifyStatus === 'NONE' || aadhaarVerifyStatus === 'FAILED') && !aadhaarOtpSent) { - } @@ -185,7 +247,9 @@ } @if (aadhaarVerifyError) { -
{{ aadhaarVerifyError }}
+
+ {{ aadhaarVerifyError }} +
} @if (aadhaarVerifyStatus === 'VERIFIED' && aadhaarVerifyName) {
@@ -203,14 +267,25 @@ @if (kycForm.get('hasPan')?.value && panAadhaarSeedingStatus) { + @if (dobChanged) { +
+ warning +
+ Re-KYC Required: Date of birth was changed. Please re-verify PAN and update the remarks + below before proceeding. +
+
+ }
- }
- @@ -141,7 +157,8 @@ }
- } @if (selectedTabIndex < totalTabs - 1) { - } @if (selectedTabIndex === totalTabs - 1 && areFormvalids()) { diff --git a/src/app/clients/edit-client/edit-client.component.scss b/src/app/clients/edit-client/edit-client.component.scss index b4c7c1f9f..771d5894b 100644 --- a/src/app/clients/edit-client/edit-client.component.scss +++ b/src/app/clients/edit-client/edit-client.component.scss @@ -48,11 +48,11 @@ .client-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); ::ng-deep { .mat-mdc-tab-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.08); + border-bottom: 1px solid rgb(0 0 0 / 8%); } .mat-mdc-tab { @@ -97,7 +97,7 @@ align-items: center; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } .action-bar-right { @@ -106,7 +106,7 @@ } .cancel-btn { - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); } /* ─── Read-only tab state ─── */ @@ -149,3 +149,34 @@ color: #ef4444 !important; border: 1px solid #fca5a5 !important; } + +/* ─── Re-KYC Alert Banner ─── */ +.rekyc-alert { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 18px; + margin: 0 0 16px; + background: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 8px; + color: #92400e; + + &__icon { + color: #f59e0b; + font-size: 22px; + width: 22px; + height: 22px; + margin-top: 1px; + flex-shrink: 0; + } + + &__text { + font-size: 13.5px; + line-height: 1.5; + + strong { + font-weight: 600; + } + } +} diff --git a/src/app/clients/edit-client/edit-client.component.ts b/src/app/clients/edit-client/edit-client.component.ts index d10951f1e..92f579192 100644 --- a/src/app/clients/edit-client/edit-client.component.ts +++ b/src/app/clients/edit-client/edit-client.component.ts @@ -9,10 +9,12 @@ /** Angular Imports */ import { Component, QueryList, ViewChild, ViewChildren, inject } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { ClientsService } from '../clients.service'; import { SettingsService } from 'app/settings/settings.service'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; /** Custom Components */ import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component'; @@ -60,6 +62,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; export class EditClientComponent { private route = inject(ActivatedRoute); private router = inject(Router); + private dialog = inject(MatDialog); private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); @@ -79,6 +82,9 @@ export class EditClientComponent { /** Track which tabs are in edit mode (false = read-only, true = editable) */ tabEditMode: Record = {}; + /** Flag: DOB was changed — requires re-KYC (PAN re-verification) */ + dobChanged = false; + /** Client data and template (merged object from resolver) */ clientDataAndTemplate: any; /** Client ID */ @@ -139,8 +145,13 @@ export class EditClientComponent { return areValids; } + /** Whether the current tab has unsaved edits */ + isCurrentTabEdited(): boolean { + return this.tabEditMode[this.selectedTabIndex] === true; + } + /** - * Next button handler — just navigates between tabs (client already exists in edit mode). + * Next button handler — saves changes via API if the current tab was edited, then moves to next tab. */ onNext() { if (this.selectedTabIndex === 0) { @@ -155,9 +166,271 @@ export class EditClientComponent { return; } } + + // If tab was edited, confirm and save before moving on + if (this.isCurrentTabEdited()) { + this.confirmAndSaveTab(); + } else { + this.selectedTabIndex = this.selectedTabIndex + 1; + } + } + + /** + * Show confirmation dialog and save the current tab's data via API. + * Shows a special warning if DOB was changed (re-KYC required). + */ + private confirmAndSaveTab() { + let heading = 'Confirm Edit'; + let dialogContext = 'Are you sure you want to save the changes'; + let dialogType = 'basic'; + + // Check if DOB changed on the General tab — warn about re-KYC + if (this.selectedTabIndex === 0) { + const changes = this.detectSensitiveFieldChanges(this.clientDataAndTemplate); + if (changes.dobChanged) { + heading = 'Re-KYC Required'; + dialogContext = + 'Date of birth has been changed. This will require PAN re-verification in the KYC section. A remark will be added automatically. Do you want to proceed'; + dialogType = 'warn'; + } + } + + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading, dialogContext, type: dialogType } + }); + + dialogRef.afterClosed().subscribe((response: any) => { + if (response?.confirm) { + this.saveCurrentTab(); + } + }); + } + + /** + * Save the current tab's data via API, then advance to the next tab. + */ + private saveCurrentTab() { + const tabIndex = this.selectedTabIndex; + + // General Details tab (always index 0) + if (tabIndex === 0) { + const locale = this.settingsService.language.code; + const dateFormat = this.settingsService.dateFormat; + const clientData = { ...this.client, dateFormat, locale }; + delete clientData.officeId; + + // Detect changes to sensitive fields before saving + const originalData = this.clientDataAndTemplate; + const changedFields = this.detectSensitiveFieldChanges(originalData); + + this.savingClient = true; + this.clientsService.updateClient(this.clientId, clientData).subscribe({ + next: () => { + this.savingClient = false; + this.tabEditMode[tabIndex] = false; + + // Log mobile/email changes as client notes for risk audit + this.logFieldChangeNotes(changedFields); + + // If DOB changed, flag re-KYC requirement and reset PAN verification + if (changedFields.dobChanged) { + this.dobChanged = true; + this.tabEditMode[1] = true; // auto-enable KYC tab for re-verification + this.flagReKycRequired(changedFields.oldDob, changedFields.newDob); + } + + // Update original data snapshot so subsequent edits compare against latest + if (changedFields.mobileChanged) { + originalData.mobileNo = changedFields.newMobile; + } + if (changedFields.emailChanged) { + originalData.emailAddress = changedFields.newEmail; + } + if (changedFields.dobChanged) { + originalData.dateOfBirth = changedFields.newDob; + } + + this.selectedTabIndex = this.selectedTabIndex + 1; + }, + error: () => { + this.savingClient = false; + } + }); + return; + } + + // KYC tab (always index 1) + if (tabIndex === 1) { + this.savingClient = true; + + // Upload photo if selected + if (this.clientKycStep?.photographFile) { + this.clientsService + .uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile) + .subscribe({ error: () => {} }); + } + // Upload signature if selected + if (this.clientKycStep?.signatureFile) { + this.clientsService + .uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile) + .subscribe({ error: () => {} }); + } + + // Save KYC data — use correct API based on KYC state + if (this.clientKycStep?.hasKycData) { + const kycData = this.clientKycStep.kycData; + const kycId = this.clientKycStep.kycId; + const isVerified = this.clientKycStep.isKycVerified; + + // Validate remarks are present for verified KYC + if (isVerified && !kycData.remarks?.trim()) { + this.savingClient = false; + this.clientKycStep.kycForm.get('remarks')?.markAsTouched(); + return; + } + + let saveObs; + if (kycId && isVerified) { + // VERIFIED → re-edit command + const { kycType, ...updatePayload } = kycData; + saveObs = this.clientsService.reEditClientKyc(this.clientId, kycId, updatePayload); + } else if (kycId) { + // Existing but not verified → normal PUT + const { kycType, ...updatePayload } = kycData; + saveObs = this.clientsService.updateClientKyc(this.clientId, kycId, updatePayload); + } else { + // New → POST + saveObs = this.clientsService.saveClientKyc(this.clientId, kycData); + } + + saveObs.subscribe({ + next: (res: any) => { + this.savingClient = false; + this.tabEditMode[tabIndex] = false; + // If re-edit, update KYC component state + if (isVerified && this.clientKycStep) { + this.clientKycStep.kycStatus = 200; + this.clientKycStep.panVerifyStatus = 'NONE'; + this.clientKycStep.panVerifyName = ''; + this.clientKycStep.panVerifiedOn = ''; + } + if (!kycId && res?.resourceId) { + this.clientKycStep.kycId = res.resourceId.toString(); + } + this.selectedTabIndex = this.selectedTabIndex + 1; + }, + error: () => { + this.savingClient = false; + } + }); + } else { + this.savingClient = false; + this.tabEditMode[tabIndex] = false; + this.selectedTabIndex = this.selectedTabIndex + 1; + } + return; + } + + // Address tab + const addressTabIdx = this.legalFormType === 2 ? 3 : 2; + if (tabIndex === addressTabIdx) { + this.savingClient = true; + if (this.clientIndiaAddressStep?.hasAddressData) { + this.saveClientAddresses(this.clientId); + } + this.savingClient = false; + this.tabEditMode[tabIndex] = false; + this.selectedTabIndex = this.selectedTabIndex + 1; + return; + } + + // Family tab + const familyTabIdx = this.legalFormType === 2 ? 4 : 3; + if (tabIndex === familyTabIdx) { + this.savingClient = true; + if (this.clientFamilyMembersStep?.hasFamilyMembers) { + this.saveClientFamilyMembers(this.clientId); + } + this.savingClient = false; + this.tabEditMode[tabIndex] = false; + this.selectedTabIndex = this.selectedTabIndex + 1; + return; + } + + // Default: just advance + this.tabEditMode[tabIndex] = false; this.selectedTabIndex = this.selectedTabIndex + 1; } + /** + * Compare current form values with original data to detect changes in DOB, mobile, email. + */ + private detectSensitiveFieldChanges(originalData: any) { + const form = this.clientGeneralForm; + const newMobile = form?.get('mobileNo')?.value || ''; + const newEmail = form?.get('emailAddress')?.value || ''; + const newDobRaw = form?.get('dateOfBirth')?.value; + const newDob = newDobRaw instanceof Date ? newDobRaw.toISOString().split('T')[0] : newDobRaw || ''; + + const oldMobile = originalData.mobileNo || ''; + const oldEmail = originalData.emailAddress || ''; + const oldDobRaw = originalData.dateOfBirth; + const oldDob = Array.isArray(oldDobRaw) + ? `${oldDobRaw[0]}-${String(oldDobRaw[1]).padStart(2, '0')}-${String(oldDobRaw[2]).padStart(2, '0')}` + : oldDobRaw instanceof Date + ? oldDobRaw.toISOString().split('T')[0] + : oldDobRaw || ''; + + return { + mobileChanged: newMobile !== oldMobile, + emailChanged: newEmail !== oldEmail, + dobChanged: newDob !== oldDob, + oldMobile, + newMobile, + oldEmail, + newEmail, + oldDob, + newDob + }; + } + + /** + * Save audit notes for mobile/email changes via the client notes API. + */ + private logFieldChangeNotes(changes: any) { + const timestamp = new Date().toISOString(); + + if (changes.mobileChanged) { + const note = `[AUDIT] Mobile number changed from "${changes.oldMobile}" to "${changes.newMobile}" on ${timestamp}`; + this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} }); + } + + if (changes.emailChanged) { + const note = `[AUDIT] Email address changed from "${changes.oldEmail}" to "${changes.newEmail}" on ${timestamp}`; + this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} }); + } + } + + /** + * When DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC. + */ + private flagReKycRequired(oldDob: string, newDob: string) { + // Reset PAN verification status so user must re-verify + if (this.clientKycStep) { + this.clientKycStep.panVerifyStatus = 'NONE'; + this.clientKycStep.panVerifyName = ''; + this.clientKycStep.panVerifiedOn = ''; + // Pre-fill remarks with re-KYC note + const remarkText = `[Re-KYC Required] Date of birth changed from ${oldDob} to ${newDob}. PAN re-verification is mandatory.`; + this.clientKycStep.kycForm.get('remarks')?.setValue(remarkText); + } + + // Also log DOB change as an audit note + const timestamp = new Date().toISOString(); + const note = `[AUDIT] Date of birth changed from "${oldDob}" to "${newDob}" on ${timestamp}. Re-KYC (PAN re-verification) required.`; + this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} }); + } + legalFormChange(eventData: { legalForm: number }) { this.legalFormType = eventData.legalForm; } @@ -185,12 +458,16 @@ export class EditClientComponent { // Upload photo if selected if (this.clientKycStep?.photographFile) { - this.clientsService.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile).subscribe({ error: () => {} }); + this.clientsService + .uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile) + .subscribe({ error: () => {} }); } // Upload signature if selected if (this.clientKycStep?.signatureFile) { - this.clientsService.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile).subscribe({ error: () => {} }); + this.clientsService + .uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile) + .subscribe({ error: () => {} }); } // Save addresses from India address step @@ -205,7 +482,20 @@ export class EditClientComponent { // Save/update KYC data if available if (this.clientKycStep?.hasKycData) { - this.clientsService.saveClientKyc(this.clientId, this.clientKycStep.kycData).subscribe({ + const kycData = this.clientKycStep.kycData; + const kycId = this.clientKycStep.kycId; + const isVerified = this.clientKycStep.isKycVerified; + let kycObs; + if (kycId && isVerified) { + const { kycType, ...updatePayload } = kycData; + kycObs = this.clientsService.reEditClientKyc(this.clientId, kycId, updatePayload); + } else if (kycId) { + const { kycType, ...updatePayload } = kycData; + kycObs = this.clientsService.updateClientKyc(this.clientId, kycId, updatePayload); + } else { + kycObs = this.clientsService.saveClientKyc(this.clientId, kycData); + } + kycObs.subscribe({ next: () => navigateBack(), error: () => navigateBack() }); @@ -225,16 +515,36 @@ export class EditClientComponent { private saveClientAddresses(clientId: string) { const addressData = this.clientIndiaAddressStep.addressData; const typeNameMap: Record = { - permanent: ['Permanent', 'Permanent Address'], - current: ['Residential', 'Current', 'Current / Residential', 'Communication'], - office: ['Office', 'Office / Work', 'Business', 'Work'], - correspondence: ['Correspondence', 'Mailing', 'Postal'] + permanent: [ + 'Permanent', + 'Permanent Address' + ], + current: [ + 'Residential', + 'Current', + 'Current / Residential', + 'Communication' + ], + office: [ + 'Office', + 'Office / Work', + 'Business', + 'Work' + ], + correspondence: [ + 'Correspondence', + 'Mailing', + 'Postal' + ] }; this.clientsService.getClientAddressTemplate().subscribe({ next: (template: any) => { const typeOptions = template?.addressTypeIdOptions || []; - for (const [tab, addr] of Object.entries(addressData)) { + for (const [ + tab, + addr + ] of Object.entries(addressData)) { if (!addr || !(addr as any).addressLine1) continue; const possibleNames = typeNameMap[tab] || [tab]; @@ -259,8 +569,8 @@ export class EditClientComponent { }; if ((addr as any).state && template?.stateProvinceIdOptions?.length) { - const stateMatch = template.stateProvinceIdOptions.find((s: any) => - s.name?.toLowerCase() === (addr as any).state?.toLowerCase() + const stateMatch = template.stateProvinceIdOptions.find( + (s: any) => s.name?.toLowerCase() === (addr as any).state?.toLowerCase() ); if (stateMatch) payload.stateProvinceId = stateMatch.id; } diff --git a/src/app/core/shell/shell.component.scss b/src/app/core/shell/shell.component.scss index ab4ec5672..6730a1f41 100644 --- a/src/app/core/shell/shell.component.scss +++ b/src/app/core/shell/shell.component.scss @@ -90,6 +90,15 @@ $color: #e2e4ec; .sidenav { overflow-x: hidden; position: relative; + + // Constrain routed page content so tables/cards shrink when sidebar opens + mifosx-content { + display: block; + min-width: 0; + max-width: 100%; + overflow-x: hidden; + box-sizing: border-box; + } } // ===== 2-Row Main Header ===== @@ -142,15 +151,28 @@ $gray-700: #334155; font-size: 11px; font-weight: 500; letter-spacing: 0.01em; - transition: background 0.12s, color 0.12s; + transition: + background 0.12s, + color 0.12s; user-select: none; border-right: 1px solid $gray-200; text-decoration: none; - font-family: 'DM Sans', 'Roboto', sans-serif; + font-family: 'DM Sans', Roboto, sans-serif; - &:last-child { border-right: none; } - &:hover { background: $brand-hover; color: $brand; } - &.active { background: $brand-light; color: $brand; font-weight: 600; } + &:last-child { + border-right: none; + } + + &:hover { + background: $brand-hover; + color: $brand; + } + + &.active { + background: $brand-light; + color: $brand; + font-weight: 600; + } &.active::before { content: ''; @@ -178,7 +200,9 @@ $gray-700: #334155; .ni-caret { font-size: 8px; opacity: 0.3; - transition: transform 0.18s, opacity 0.18s; + transition: + transform 0.18s, + opacity 0.18s; flex-shrink: 0; .ni:hover & { @@ -211,7 +235,7 @@ $gray-700: #334155; min-height: 34px !important; padding: 0 14px !important; color: $gray-700 !important; - font-family: 'DM Sans', 'Roboto', sans-serif !important; + font-family: 'DM Sans', Roboto, sans-serif !important; &:hover { background: $brand-hover !important; @@ -266,7 +290,7 @@ $gray-700: #334155; width: 860px; max-width: 95vw; padding: 20px 24px; - box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22); + box-shadow: 0 6px 24px rgb(0 0 0 / 22%); border-radius: 0 0 10px 10px; z-index: 1000; border-top: 3px solid #0d47a1; @@ -307,7 +331,7 @@ $gray-700: #334155; outline: none; border-color: #0d47a1; background: #fff; - box-shadow: 0 0 0 3px rgba(13, 71, 161, 0.1); + box-shadow: 0 0 0 3px rgb(13 71 161 / 10%); } } diff --git a/src/app/shared/confirmation-dialog/confirmation-dialog.component.html b/src/app/shared/confirmation-dialog/confirmation-dialog.component.html index 4d94392d2..a3379a1ab 100644 --- a/src/app/shared/confirmation-dialog/confirmation-dialog.component.html +++ b/src/app/shared/confirmation-dialog/confirmation-dialog.component.html @@ -6,13 +6,42 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -

{{ data.heading }}

-
-

{{ data.dialogContext }} ?

+
+ +
+
+ @switch (data.type) { + @case ('warn') { + + } + @case ('Strong') { + + } + @default { + + } + } +
+

{{ data.heading }}

+
+ + + +

{{ data.dialogContext }}

+
+ + + + + +
- - - - diff --git a/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss b/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss index af9f4c075..c8fd726af 100644 --- a/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss +++ b/src/app/shared/confirmation-dialog/confirmation-dialog.component.scss @@ -5,3 +5,145 @@ * 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/. */ + +$primary: #1a73e8; +$primary-light: #e8f0fe; +$amber: #f59e0b; +$amber-light: #fef7e0; +$red: #ef4444; +$red-light: #fef2f2; +$gray-100: #f1f3f4; +$gray-200: #e8eaed; +$gray-500: #9aa0a6; +$gray-700: #5f6368; +$gray-900: #202124; +$white: #fff; + +.confirm-dialog { + display: flex; + flex-direction: column; + max-width: 460px; + min-width: 380px; +} + +// ─── Header ─── +.confirm-header { + display: flex; + align-items: center; + gap: 14px; + padding: 22px 24px 16px; + border-bottom: 1px solid $gray-200; + + &__icon-wrap { + width: 42px; + height: 42px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .confirm-dialog--basic & { + background: $primary-light; + + i { + color: $primary; + } + } + + .confirm-dialog--warn & { + background: $amber-light; + + i { + color: $amber; + } + } + + .confirm-dialog--Strong & { + background: $red-light; + + i { + color: $red; + } + } + + i { + font-size: 18px; + } + } + + &__title { + margin: 0; + padding: 0; + font-size: 17px; + font-weight: 700; + color: $gray-900; + letter-spacing: -0.2px; + line-height: 1.25; + } +} + +// ─── Body ─── +.confirm-body { + padding: 18px 24px !important; + overflow: visible !important; + max-height: none !important; + + &__text { + font-size: 13.5px; + line-height: 1.65; + color: $gray-700; + margin: 0; + } +} + +// ─── Actions ─── +.confirm-actions { + padding: 14px 24px 20px !important; + border-top: 1px solid $gray-200; + margin: 0 !important; + display: flex !important; + justify-content: flex-end !important; + align-items: center !important; + gap: 10px !important; + + &__cancel { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + padding: 0 20px !important; + height: 38px !important; + color: $gray-700 !important; + border-color: $gray-200 !important; + } + + &__confirm { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + padding: 0 24px !important; + height: 38px !important; + letter-spacing: 0.2px; + color: $white !important; + + &--basic { + background: $primary !important; + box-shadow: 0 1px 3px rgba($primary, 0.3) !important; + } + + &--warn { + background: $amber !important; + box-shadow: 0 1px 3px rgba($amber, 0.3) !important; + } + + &--Strong { + background: $red !important; + box-shadow: 0 1px 3px rgba($red, 0.3) !important; + } + } + + &__btn-icon { + margin-right: 6px; + font-size: 12px; + } +} diff --git a/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts b/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts index 6785d6d54..dfa51070f 100644 --- a/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts +++ b/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts @@ -7,22 +7,21 @@ */ /** Angular Imports */ -import { Component, OnInit, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA, - MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog'; import { DialogData } from 'app/core/dialogs/dialog-data.model'; -import { Dialogs } from 'app/core/dialogs/dialogs'; import { CdkScrollable } from '@angular/cdk/scrolling'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** - * Delete dialog component. + * Confirmation dialog component. + * Supports types: 'basic' (blue), 'warn' (amber), 'Strong' (red/destructive). */ @Component({ selector: 'mifosx-confirmation-dialog', @@ -30,25 +29,13 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./confirmation-dialog.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatDialogTitle, CdkScrollable, MatDialogContent, MatDialogActions, MatDialogClose ] }) -export class ConfirmationDialogComponent implements OnInit { +export class ConfirmationDialogComponent { dialogRef = inject>(MatDialogRef); - private dialogs = inject(Dialogs); data = inject(MAT_DIALOG_DATA); - - color: string; - - ngOnInit() { - this.setColor(); - } - - setColor() { - this.color = this.dialogs.setColor(this.data.type); - } } diff --git a/src/app/shared/styles/list-layout.scss b/src/app/shared/styles/list-layout.scss index dba7ea93a..8af67d841 100644 --- a/src/app/shared/styles/list-layout.scss +++ b/src/app/shared/styles/list-layout.scss @@ -23,6 +23,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; border-radius: 12px; border: 1px solid $border; box-shadow: 0 1px 3px rgb(0 0 0 / 5%); + min-width: 0; + max-width: 100%; + box-sizing: border-box; // Legacy layout support .layout-column, @@ -45,14 +48,21 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; flex: 1; min-width: 0; - &.m-r-30 { margin-right: 12px; } + &.m-r-30 { + margin-right: 12px; + } mat-form-field { min-width: 200px; flex: 1; - &.search-box { width: auto; } - + mat-form-field { margin-left: 12px; } + &.search-box { + width: auto; + } + + + mat-form-field { + margin-left: 12px; + } } mat-checkbox { @@ -60,7 +70,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; flex-shrink: 0; margin-left: 8px; - &.m-r-10 { margin-right: 8px; } + &.m-r-10 { + margin-right: 8px; + } } } @@ -70,14 +82,18 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; flex-shrink: 0; margin-left: auto; - &.gap-25px { gap: 10px; } + &.gap-25px { + gap: 10px; + } button { white-space: nowrap; fa-icon { &.m-r-10, - &.m-r-20 { margin-right: 6px; } + &.m-r-20 { + margin-right: 6px; + } } } } @@ -92,11 +108,16 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; .select-row { cursor: pointer; - &:hover { background-color: $hover-bg; } + + &:hover { + background-color: $hover-bg; + } } th, - td { padding: 10px 16px; } + td { + padding: 10px 16px; + } th { font-weight: 600; @@ -110,7 +131,10 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; tr { border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } + + &:last-child { + border-bottom: none; + } } td { @@ -125,7 +149,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; background: transparent; } - mat-progress-bar { margin: 0; } + mat-progress-bar { + margin: 0; + } } // ===== Modern Table Header ===== @@ -167,10 +193,21 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; span { border-radius: 2px; - &:nth-child(1) { background: $brand; } - &:nth-child(2) { background: #0ea5e9; } - &:nth-child(3) { background: #10b981; } - &:nth-child(4) { background: #cbd5e1; } + &:nth-child(1) { + background: $brand; + } + + &:nth-child(2) { + background: #0ea5e9; + } + + &:nth-child(3) { + background: #10b981; + } + + &:nth-child(4) { + background: #cbd5e1; + } } } } @@ -198,7 +235,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; max-width: 360px; ::ng-deep { - .mat-mdc-form-field-flex { height: 38px; } + .mat-mdc-form-field-flex { + height: 38px; + } .mat-mdc-form-field-infix { padding: 6px 0; @@ -206,8 +245,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } .mat-mdc-form-field-icon-prefix { @@ -215,7 +259,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; color: $text-muted; } - input { font-size: 13px; } + input { + font-size: 13px; + } } } @@ -237,7 +283,10 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; height: 36px; padding: 0 16px; - fa-icon { margin-right: 6px; font-size: 12px; } + fa-icon { + margin-right: 6px; + font-size: 12px; + } } .btn-import { @@ -259,7 +308,7 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; .table-container { border: 1px solid $border; border-radius: 10px; - overflow: hidden; + overflow: hidden auto; } .table-loader { @@ -294,7 +343,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; border-bottom: 1px solid #f1f5f9; transition: background 0.1s; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } &:nth-child(even) { background: #fafbfc; @@ -314,7 +365,11 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; font-weight: 400; color: $text; padding: 0 12px; - font-family: 'Inter', 'Roboto', -apple-system, sans-serif; + font-family: + Inter, + Roboto, + -apple-system, + sans-serif; } } } @@ -324,9 +379,15 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; font-weight: 500; font-size: 13px; cursor: pointer; - font-family: 'Inter', 'Roboto', -apple-system, sans-serif; + font-family: + Inter, + Roboto, + -apple-system, + sans-serif; - &:hover { text-decoration: underline; } + &:hover { + text-decoration: underline; + } } .cell-mono { @@ -350,7 +411,11 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; letter-spacing: 0.02em; text-transform: capitalize; white-space: nowrap; - font-family: 'Inter', 'Roboto', -apple-system, sans-serif; + font-family: + Inter, + Roboto, + -apple-system, + sans-serif; &::before { content: ''; @@ -367,7 +432,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; background: #ecfdf5 !important; color: #059669 !important; - &::before { background: #059669; } + &::before { + background: #059669; + } } .status-active-overdue, @@ -375,7 +442,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; background: #fef2f2 !important; color: #dc2626 !important; - &::before { background: #dc2626; } + &::before { + background: #dc2626; + } } .status-submitted-and-pending-approval, @@ -383,14 +452,18 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; background: #fffbeb !important; color: #d97706 !important; - &::before { background: #d97706; } + &::before { + background: #d97706; + } } .status-overpaid { background: #eff6ff !important; color: #2563eb !important; - &::before { background: #2563eb; } + &::before { + background: #2563eb; + } } } @@ -402,8 +475,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; vertical-align: middle; margin-right: 6px; - &.person { color: #2563eb; } - &.entity { color: #7c3aed; } + &.person { + color: #2563eb; + } + + &.entity { + color: #7c3aed; + } } .legal-form-badge { @@ -550,10 +628,15 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; min-width: 150px; margin-bottom: 8px; - + mat-form-field { margin-left: 0; } + + mat-form-field { + margin-left: 0; + } } - mat-checkbox { margin-left: 0; margin-top: 8px; } + mat-checkbox { + margin-left: 0; + margin-top: 8px; + } } .action-button { @@ -561,25 +644,50 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; width: 100%; justify-content: center; - &.gap-25px { gap: 10px; } + &.gap-25px { + gap: 10px; + } } } } - .table-actions { flex-direction: column; align-items: stretch; } - .table-search { max-width: 100%; } - .table-buttons { margin-left: 0; justify-content: stretch; } - .table-buttons button { flex: 1; } + .table-actions { + flex-direction: column; + align-items: stretch; + } + + .table-search { + max-width: 100%; + } + + .table-buttons { + margin-left: 0; + justify-content: stretch; + } + + .table-buttons button { + flex: 1; + } } // Legacy table styles table { width: 100%; - tr.select-row:hover { cursor: pointer; } + tr.select-row:hover { + cursor: pointer; + } } // Utility classes -.m-r-10 { margin-right: 10px !important; } -.m-r-20 { margin-right: 20px !important; } -.m-r-30 { margin-right: 30px !important; } +.m-r-10 { + margin-right: 10px !important; +} + +.m-r-20 { + margin-right: 20px !important; +} + +.m-r-30 { + margin-right: 30px !important; +} diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 216b49c93..9dd2af68c 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -1,9 +1,17 @@ +/** + * 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/. + */ + // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260404', - 'hash': 'gab0111142' + 'version': '260407', + 'hash': 'gb890d24bb' }, 'allow_switching_backend_instance': true };