Create User dialog now has a mandatory Email/Mobile OTP delivery method picker (styled as a pill toggle) alongside the existing fields. Adds a new post-login agreement acceptance dialog: right after the existing 2FA flow (TwoFactorAuthenticationComponent) succeeds, or immediately after login for users without 2FA, the user must accept the bank's current agreement/disclaimer at least once before reaching the app. Also adds a "Manage User Agreement" admin screen (System section) to publish versioned agreement text without a code deploy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
281 lines
11 KiB
HTML
281 lines
11 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="login-page">
|
|
<div class="login-split">
|
|
<!-- ══ LEFT PANEL — SLIDER ══ -->
|
|
<div class="left-panel">
|
|
<div class="left-panel__dots-bg"></div>
|
|
|
|
<!-- Slides -->
|
|
<div class="slides-wrap">
|
|
@for (slide of slides; track slide.tag; let i = $index) {
|
|
<div class="slide" [class.active]="currentSlide === i" [ngClass]="'s' + (i + 1)">
|
|
<div class="slide__glow"></div>
|
|
<span class="slide__tag"><span class="slide__pulse"></span>{{ slide.tag }}</span>
|
|
|
|
<!-- LincShield SVG for FRM slide -->
|
|
@if (i === 1) {
|
|
<div class="lincshield-logo">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 180" width="420" height="100">
|
|
<defs>
|
|
<linearGradient id="blueGD2" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color: #4d90e8" />
|
|
<stop offset="100%" style="stop-color: #2563c8" />
|
|
</linearGradient>
|
|
<linearGradient id="greenGD2" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color: #34d05c" />
|
|
<stop offset="100%" style="stop-color: #28b84e" />
|
|
</linearGradient>
|
|
<linearGradient id="shieldGD2" x1="0%" y1="0%" x2="60%" y2="100%">
|
|
<stop offset="0%" style="stop-color: #34d05c" />
|
|
<stop offset="100%" style="stop-color: #1a9040" />
|
|
</linearGradient>
|
|
<filter id="glow3">
|
|
<feGaussianBlur stdDeviation="3" result="b" />
|
|
<feMerge>
|
|
<feMergeNode in="b" />
|
|
<feMergeNode in="SourceGraphic" />
|
|
</feMerge>
|
|
</filter>
|
|
</defs>
|
|
<path
|
|
d="M14 22 Q14 19 17 17 L44 8 Q47 7 50 8 L77 17 Q80 19 80 22 L80 72 Q80 100 47 114 Q14 100 14 72 Z"
|
|
fill="url(#shieldGD2)"
|
|
filter="url(#glow3)"
|
|
/>
|
|
<path
|
|
d="M22 28 Q22 26 24 25 L46 18 Q48 17 50 18 L72 25 Q74 26 74 28 L74 71 Q74 93 47 105 Q20 93 20 71 Z"
|
|
fill="none"
|
|
stroke="rgba(255,255,255,0.15)"
|
|
stroke-width="1.5"
|
|
/>
|
|
<path
|
|
d="M33 65 L44 76 L66 50"
|
|
stroke="white"
|
|
stroke-width="6"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
fill="none"
|
|
/>
|
|
<path
|
|
d="M35 42 Q47 34 59 42"
|
|
stroke="rgba(255,255,255,0.7)"
|
|
stroke-width="3"
|
|
stroke-linecap="round"
|
|
fill="none"
|
|
/>
|
|
<polygon points="59,37 59,47 65,42" fill="rgba(255,255,255,0.7)" />
|
|
<text
|
|
x="98"
|
|
y="105"
|
|
font-family="Georgia, 'Times New Roman', serif"
|
|
font-size="96"
|
|
font-weight="900"
|
|
font-style="italic"
|
|
fill="url(#blueGD2)"
|
|
letter-spacing="-2"
|
|
>
|
|
Linc
|
|
</text>
|
|
<path
|
|
d="M334 48 Q344 35 358 46"
|
|
stroke="#34D05C"
|
|
stroke-width="6.5"
|
|
stroke-linecap="round"
|
|
fill="none"
|
|
/>
|
|
<polygon points="356,38 362,50 350,49" fill="#34D05C" />
|
|
<text
|
|
x="356"
|
|
y="105"
|
|
font-family="Georgia, 'Times New Roman', serif"
|
|
font-size="96"
|
|
font-weight="900"
|
|
font-style="italic"
|
|
fill="url(#greenGD2)"
|
|
letter-spacing="-2"
|
|
>
|
|
Shield
|
|
</text>
|
|
<line x1="98" y1="122" x2="752" y2="122" stroke="#1a2d47" stroke-width="1" />
|
|
<text
|
|
x="98"
|
|
y="148"
|
|
font-family="'Trebuchet MS', 'Segoe UI', sans-serif"
|
|
font-size="18"
|
|
font-weight="700"
|
|
letter-spacing="6"
|
|
fill="#4D90E8"
|
|
>
|
|
LINK
|
|
</text>
|
|
<circle cx="178" cy="140" r="3" fill="#34D05C" />
|
|
<text
|
|
x="190"
|
|
y="148"
|
|
font-family="'Trebuchet MS', 'Segoe UI', sans-serif"
|
|
font-size="18"
|
|
font-weight="700"
|
|
letter-spacing="6"
|
|
fill="#8899bb"
|
|
>
|
|
DETECT
|
|
</text>
|
|
<circle cx="312" cy="140" r="3" fill="#34D05C" />
|
|
<text
|
|
x="324"
|
|
y="148"
|
|
font-family="'Trebuchet MS', 'Segoe UI', sans-serif"
|
|
font-size="18"
|
|
font-weight="700"
|
|
letter-spacing="6"
|
|
fill="#34D05C"
|
|
>
|
|
PROTECT.
|
|
</text>
|
|
</svg>
|
|
</div>
|
|
} @else {
|
|
<span class="slide__icon">{{ slide.icon }}</span>
|
|
}
|
|
|
|
<h2 class="slide__title" [innerHTML]="slide.title"></h2>
|
|
<p class="slide__desc">{{ slide.desc }}</p>
|
|
<div class="slide__stats">
|
|
@for (stat of slide.stats; track stat.label) {
|
|
<div class="stat-chip">
|
|
<div class="stat-chip__value">{{ stat.value }}</div>
|
|
<div class="stat-chip__label">{{ stat.label }}</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
<!-- Flow diagram -->
|
|
<div class="slide__flow">
|
|
@for (node of slide.flow; track node; let last = $last) {
|
|
<div class="flow-node">{{ node }}</div>
|
|
@if (!last) {
|
|
<div class="flow-arrow"></div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Nav dots -->
|
|
<div class="slide-nav">
|
|
@for (slide of slides; track slide.tag; let i = $index) {
|
|
<button class="slide-dot" [class.active]="currentSlide === i" (click)="goToSlide(i)"></button>
|
|
}
|
|
</div>
|
|
<div class="slide-progress" [style.animation-duration]="slideDuration + 'ms'"></div>
|
|
</div>
|
|
|
|
<!-- ══ RIGHT PANEL — LOGIN ══ -->
|
|
<div class="right-panel">
|
|
<!-- Settings trigger -->
|
|
<button class="settings-btn" (click)="toggleSettingsPanel()" matTooltip="Settings">
|
|
<mifosx-m3-icon [name]="settingsPanelOpen ? 'close' : 'settings'"></mifosx-m3-icon>
|
|
</button>
|
|
|
|
<!-- Settings panel -->
|
|
@if (settingsPanelOpen) {
|
|
<div class="settings-overlay" (click)="toggleSettingsPanel()"></div>
|
|
<div class="settings-panel" @settingsAnimation>
|
|
<div class="settings-panel__header">
|
|
<mifosx-m3-icon name="tune"></mifosx-m3-icon>
|
|
<h3>{{ 'labels.heading.Settings' | translate }}</h3>
|
|
</div>
|
|
<div class="settings-panel__body">
|
|
@if (showTenantSelector) {
|
|
<div class="settings-item">
|
|
<span class="settings-item__label">
|
|
<mifosx-m3-icon name="domain"></mifosx-m3-icon>
|
|
{{ 'labels.inputs.Tenant' | translate }}
|
|
</span>
|
|
<mifosx-tenant-selector></mifosx-tenant-selector>
|
|
</div>
|
|
}
|
|
<div class="settings-item">
|
|
<span class="settings-item__label">
|
|
<mifosx-m3-icon name="dark_mode"></mifosx-m3-icon>
|
|
{{ 'labels.heading.Theme' | translate }}
|
|
</span>
|
|
<mifosx-theme-toggle></mifosx-theme-toggle>
|
|
</div>
|
|
<div class="settings-item">
|
|
<span class="settings-item__label">
|
|
<mifosx-m3-icon name="language"></mifosx-m3-icon>
|
|
{{ 'labels.heading.Language' | translate }}
|
|
</span>
|
|
<mifosx-language-selector appearance="outline" [showLabel]="false"></mifosx-language-selector>
|
|
</div>
|
|
@if (allowServerSwitch()) {
|
|
<div class="settings-item">
|
|
<span class="settings-item__label">
|
|
<mifosx-m3-icon name="dns"></mifosx-m3-icon>
|
|
{{ 'labels.heading.Server' | translate }}
|
|
</span>
|
|
<mifosx-server-selector appearance="outline" [showLabel]="false"></mifosx-server-selector>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Brand -->
|
|
<div class="brand">
|
|
<div class="brand__logo">
|
|
@if (!themeDarkEnabled) {
|
|
<img [src]="logoPath" (error)="onLogoError()" alt="{{ 'APP_NAME' | translate }}" />
|
|
} @else {
|
|
<img [src]="logoPathDark" (error)="onLogoErrorDark()" alt="{{ 'APP_NAME' | translate }}" />
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Welcome -->
|
|
<div class="welcome">
|
|
<h2>{{ 'labels.heading.Welcome Back' | translate }}</h2>
|
|
<p>{{ 'labels.text.Sign in to continue' | translate }}</p>
|
|
</div>
|
|
|
|
<!-- Login Forms -->
|
|
@if (!resetPassword && !twoFactorAuthenticationRequired && !agreementAcceptanceRequired) {
|
|
<mifosx-login-form></mifosx-login-form>
|
|
}
|
|
@if (resetPassword) {
|
|
<mifosx-reset-password></mifosx-reset-password>
|
|
}
|
|
<!-- 2FA and the post-login agreement dialog open as non-dismissable dialogs from login.component.ts -->
|
|
|
|
<div class="right-footer">RBI Licensed · PCI-DSS Compliant · ISO 27001 Certified</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="login-footer">
|
|
<span>🇮🇳</span>
|
|
<span class="login-footer__text">
|
|
Compliant with guidelines issued by
|
|
<strong>Institute for Development and Research in Banking Technology (IDRBT)</strong>
|
|
— Established by Reserve Bank of India
|
|
</span>
|
|
<span class="login-footer__sep">|</span>
|
|
<span class="login-footer__text">© 2025 {{ 'APP_NAME' | translate }}. All rights reserved.</span>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- Resource Menus (kept for compatibility) -->
|
|
<mat-menu #resourcesMenu="matMenu">
|
|
<a [href]="'userManual' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
|
|
{{ 'labels.links.User Manual' | translate }}
|
|
</a>
|
|
</mat-menu>
|