diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html index f21112a45..7e928ce87 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html @@ -298,9 +298,15 @@ Relationship @for (rel of data.options.relationshipIdOptions; track rel) { - {{ rel.name }} + + {{ rel.name }} + @if (disabledRelationshipIds.has(rel.id)) { + · already added + } + } + Father, Mother and Spouse can only be added once. @if (entryMode === 'manual') { diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss index 90aa50ae5..4805baf5d 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss @@ -28,6 +28,14 @@ margin-bottom: 16px; } +/* Suffix shown next to disabled relationship options (Father/Mother/Spouse already added). */ +.fm-rel-taken { + margin-left: 4px; + font-size: 11px; + font-style: italic; + color: #94a3b8; +} + .fm-mode-btn { display: inline-flex; align-items: center; diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts index 06dee441f..7412f6876 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts @@ -86,6 +86,42 @@ export class ClientFamilyMemberDialogComponent implements OnInit { return rel?.name?.toUpperCase() === 'SPOUSE'; } + /** + * Relationships that may appear at most once per client. Names compared + * case-insensitively against the option list returned by the backend. + */ + private static readonly UNIQUE_RELATIONSHIPS = new Set([ + 'FATHER', + 'MOTHER', + 'SPOUSE' + ]); + + /** + * Set of relationshipId values that are already taken by another family + * member and should therefore be disabled in the dropdown. The member + * currently being edited is excluded so its existing relationship stays + * selectable. Empty set in Add mode when no unique relations exist yet. + */ + get disabledRelationshipIds(): Set { + const disabled = new Set(); + const existing = this.data?.existingMembers || []; + const options = this.data?.options?.relationshipIdOptions || []; + if (!existing.length || !options.length) return disabled; + const editingId = this.data?.context === 'Edit' ? this.data?.member?.id : undefined; + for (const m of existing) { + // Skip the member being edited so it can keep its current selection. + if (editingId && m.id === editingId) continue; + const relId = typeof m.relationshipId === 'object' ? m.relationshipId?.id : m.relationshipId; + if (relId == null) continue; + const rel = options.find((r: any) => r.id === relId); + const name = (rel?.name || '').toUpperCase(); + if (ClientFamilyMemberDialogComponent.UNIQUE_RELATIONSHIPS.has(name)) { + disabled.add(relId); + } + } + return disabled; + } + /** Whether dialog is in View-only mode */ get isViewMode(): boolean { return this.data.context === 'View'; diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts index ec60d8867..ed40bd081 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts @@ -132,7 +132,11 @@ export class ClientFamilyMembersStepComponent implements OnInit { addFamilyMember() { const ref = this.dialog.open(ClientFamilyMemberDialogComponent, { - data: { context: 'Add', options: this.clientTemplate.familyMemberOptions }, + data: { + context: 'Add', + options: this.clientTemplate.familyMemberOptions, + existingMembers: this.clientFamilyMembers + }, width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel' @@ -146,7 +150,12 @@ export class ClientFamilyMembersStepComponent implements OnInit { viewFamilyMember(member: any) { this.dialog.open(ClientFamilyMemberDialogComponent, { - data: { context: 'View', member, options: this.clientTemplate.familyMemberOptions }, + data: { + context: 'View', + member, + options: this.clientTemplate.familyMemberOptions, + existingMembers: this.clientFamilyMembers + }, width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel' @@ -155,7 +164,12 @@ export class ClientFamilyMembersStepComponent implements OnInit { editFamilyMember(member: any, index: number) { const ref = this.dialog.open(ClientFamilyMemberDialogComponent, { - data: { context: 'Edit', member, options: this.clientTemplate.familyMemberOptions }, + data: { + context: 'Edit', + member, + options: this.clientTemplate.familyMemberOptions, + existingMembers: this.clientFamilyMembers + }, width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel' diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 37a33e9a2..787b209a1 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -13,6 +13,15 @@

{{ 'labels.text.Welcome' | translate }}, {{ username }} + @if (dscMapped) { + + + + + + DSC Verified + + }

{{ tenant | titlecase }} · CBSNexora Core Banking Platform

diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index c736676ea..c2048c15f 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -54,6 +54,10 @@ $accent: #0ea5e9; font-size: 20px; font-weight: 400; color: rgb(255 255 255 / 90%); + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; strong { font-weight: 700; @@ -62,6 +66,70 @@ $accent: #0ea5e9; } } +/* ── DSC Verified badge (welcome banner) ── */ +.dsc-badge { + display: inline-flex; + align-items: center; + gap: 8px; + margin-left: 10px; + padding: 4px 12px 4px 6px; + background: linear-gradient(135deg, rgb(34 197 94 / 18%) 0%, rgb(34 197 94 / 8%) 100%); + border: 1px solid rgb(74 222 128 / 35%); + border-radius: 999px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.02em; + color: #bbf7d0; + cursor: default; + transition: + transform 0.18s ease, + background 0.18s ease; + + &:hover { + transform: translateY(-1px); + background: linear-gradient(135deg, rgb(34 197 94 / 28%) 0%, rgb(34 197 94 / 14%) 100%); + } + + &__shield { + position: relative; + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgb(22 163 74 / 90%); + border-radius: 6px; + color: #fff; + font-size: 13px; + flex-shrink: 0; + box-shadow: 0 1px 3px rgb(15 118 50 / 35%); + } + + &__check { + position: absolute; + right: -4px; + bottom: -4px; + width: 14px; + height: 14px; + background: #16a34a; + color: #fff; + border: 2px solid #1a5276; /* matches the banner's right edge so the ring blends in */ + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 8px; + line-height: 1; + } + + &__label { + color: #ecfdf5; + text-transform: uppercase; + font-size: 10.5px; + letter-spacing: 0.06em; + } +} + .welcome-sub { margin: 4px 0 0; font-size: 12px; diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index d2438cf4f..66e68ba47 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -25,11 +25,13 @@ import { AuthenticationService } from '../core/authentication/authentication.ser import { PopoverService } from '../configuration-wizard/popover/popover.service'; import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service'; import { SettingsService } from 'app/settings/settings.service'; +import { DscService } from 'app/users/dsc/dsc.service'; /** Custom Components */ import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatAutocompleteTrigger, MatAutocomplete, MatOption } from '@angular/material/autocomplete'; +import { MatTooltip } from '@angular/material/tooltip'; import { AsyncPipe, DatePipe, TitleCasePipe } from '@angular/common'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -45,6 +47,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FaIconComponent, MatAutocompleteTrigger, MatAutocomplete, + MatTooltip, AsyncPipe, DatePipe, TitleCasePipe @@ -58,6 +61,7 @@ export class HomeComponent implements OnInit, AfterViewInit { private configurationWizardService = inject(ConfigurationWizardService); private popoverService = inject(PopoverService); private settingsService = inject(SettingsService); + private dscService = inject(DscService); /** Username of authenticated user. */ username: string; @@ -65,6 +69,10 @@ export class HomeComponent implements OnInit, AfterViewInit { tenant: string; /** Today's date */ today = new Date(); + /** True when the signed-in user has at least one ACTIVE DSC certificate mapped. */ + dscMapped = false; + /** Masked serial of the ACTIVE DSC, surfaced in the welcome badge tooltip. */ + dscSerial = ''; /** Activity Form. */ activityForm: any; /** Search Text. */ @@ -95,6 +103,7 @@ export class HomeComponent implements OnInit, AfterViewInit { this.username = credentials.username; this.tenant = this.tenantIdentifier(); this.setFilteredActivities(); + this.loadDscStatus(); if (!this.authenticationService.hasDialogBeenShown()) { this.dialog.open(WarningDialogComponent, { disableClose: false, @@ -104,6 +113,26 @@ export class HomeComponent implements OnInit, AfterViewInit { } } + /** + * Surface a small "DSC verified" badge in the welcome banner when the + * signed-in user has at least one ACTIVE DSC certificate. Silent on + * error / when nothing is mapped — the badge simply doesn't render. + */ + private loadDscStatus(): void { + this.dscService.listMyDscs().subscribe({ + next: (dscs) => { + const active = (dscs || []).find((d) => (d?.certStatus || '').toUpperCase() === 'ACTIVE'); + if (active) { + this.dscMapped = true; + this.dscSerial = active.dscSerialNoMasked || active.dscSerialNoFull || ''; + } + }, + error: () => { + // Silent — endpoint may be unavailable; we just don't show the badge. + } + }); + } + /** * Sets filtered activities for autocomplete. */