Merge branch 'dev' of https://git.lincpay.tech/CBSNexora/cbsnexora into dev
# Conflicts: # src/environments/.env.ts
This commit is contained in:
@@ -13,6 +13,7 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
// Not Found Component
|
// Not Found Component
|
||||||
import { NotFoundComponent } from './not-found/not-found.component';
|
import { NotFoundComponent } from './not-found/not-found.component';
|
||||||
import { CallbackComponent } from './zitadel/callback/callback.component';
|
import { CallbackComponent } from './zitadel/callback/callback.component';
|
||||||
|
import { PlatformMenuAccessComponent } from './platform-admin/menu-access/menu-access.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback to this route when no prior route is matched.
|
* Fallback to this route when no prior route is matched.
|
||||||
@@ -22,6 +23,12 @@ const routes: Routes = [
|
|||||||
path: 'callback',
|
path: 'callback',
|
||||||
component: CallbackComponent
|
component: CallbackComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Platform-operator only — outside the normal login/tenant flow on purpose, see
|
||||||
|
// PlatformMenuAccessComponent's own doc comment for why that is safe.
|
||||||
|
path: 'platform-admin/menu-access',
|
||||||
|
component: PlatformMenuAccessComponent
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
component: NotFoundComponent
|
component: NotFoundComponent
|
||||||
|
|||||||
@@ -473,7 +473,7 @@
|
|||||||
════════ END OLD ORDER ════════
|
════════ END OLD ORDER ════════
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@if (!hideShellLayout) {
|
@if (!hideShellLayout && menuAccessReady) {
|
||||||
<div class="main-header">
|
<div class="main-header">
|
||||||
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
<!-- Row 1 — Client Master · CFCFRMS · Approvals · Cashier · Clearing · Deposits · Advances · Share · NPA · General Ledger · Sub Ledger -->
|
||||||
<div class="nav-row r1">
|
<div class="nav-row r1">
|
||||||
@@ -524,7 +524,12 @@
|
|||||||
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<a class="ni" tabindex="0" [matMenuTriggerFor]="loanOriginationMenu" *mifosxHasPermission="'MENU_ADVANCES'">
|
<a
|
||||||
|
class="ni"
|
||||||
|
tabindex="0"
|
||||||
|
[matMenuTriggerFor]="loanOriginationMenu"
|
||||||
|
*mifosxHasPermission="'MENU_LOAN_ORIGINATION'"
|
||||||
|
>
|
||||||
<fa-icon icon="clipboard-check" class="ni-ico"></fa-icon>Loan Origination
|
<fa-icon icon="clipboard-check" class="ni-ico"></fa-icon>Loan Origination
|
||||||
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
@@ -586,7 +591,7 @@
|
|||||||
class="ni"
|
class="ni"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
(click)="investmentMenuOpen = !investmentMenuOpen; rbiMenuOpen = false; glMenuOpen = false"
|
(click)="investmentMenuOpen = !investmentMenuOpen; rbiMenuOpen = false; glMenuOpen = false"
|
||||||
*mifosxHasPermission="'MENU_RBI'"
|
*mifosxHasPermission="'MENU_INVESTMENT'"
|
||||||
>
|
>
|
||||||
<fa-icon icon="chart-line" class="ni-ico"></fa-icon>Investment
|
<fa-icon icon="chart-line" class="ni-ico"></fa-icon>Investment
|
||||||
<fa-icon [icon]="investmentMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
<fa-icon [icon]="investmentMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { Router, NavigationEnd } from '@angular/router';
|
import { Router, NavigationEnd } from '@angular/router';
|
||||||
import { AuthenticationService } from '../authentication/authentication.service';
|
import { AuthenticationService } from '../authentication/authentication.service';
|
||||||
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
import { ClerkUiStateService } from '../../cashier/clerk-ui-state.service';
|
||||||
|
import { TenantMenuAccessService } from '../../directives/has-permission/tenant-menu-access.service';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
@@ -66,6 +67,7 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private authService = inject(AuthenticationService);
|
private authService = inject(AuthenticationService);
|
||||||
private clerkUiStateService = inject(ClerkUiStateService);
|
private clerkUiStateService = inject(ClerkUiStateService);
|
||||||
|
private tenantMenuAccessService = inject(TenantMenuAccessService);
|
||||||
|
|
||||||
/** Subscription to breakpoint observer for handset. */
|
/** Subscription to breakpoint observer for handset. */
|
||||||
isHandset$: Observable<boolean> = this.breakpointObserver
|
isHandset$: Observable<boolean> = this.breakpointObserver
|
||||||
@@ -79,6 +81,10 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
progressBar$: Subscription;
|
progressBar$: Subscription;
|
||||||
/** Hides nav/sidebar for full-screen cashier routes. */
|
/** Hides nav/sidebar for full-screen cashier routes. */
|
||||||
hideShellLayout = false;
|
hideShellLayout = false;
|
||||||
|
/** True once this tenant's own menu-access ceiling has loaded — the nav waits on this so a menu
|
||||||
|
* the tenant has turned off is never shown for an instant before disappearing. See
|
||||||
|
* TenantMenuAccessService. */
|
||||||
|
menuAccessReady = false;
|
||||||
private routeSub: Subscription;
|
private routeSub: Subscription;
|
||||||
|
|
||||||
/** Account Facilities — populated from ClerkUiStateService when account loads */
|
/** Account Facilities — populated from ClerkUiStateService when account loads */
|
||||||
@@ -143,6 +149,11 @@ export class ShellComponent implements OnInit, OnDestroy {
|
|||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.tenantMenuAccessService.load().subscribe(() => {
|
||||||
|
this.menuAccessReady = true;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
// Cashier-only users: hide shell. Clerk/dashboard: also fullscreen by default.
|
// Cashier-only users: hide shell. Clerk/dashboard: also fullscreen by default.
|
||||||
const checkLayout = () => {
|
const checkLayout = () => {
|
||||||
const url = this.router.url;
|
const url = this.router.url;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angula
|
|||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
|
import { TenantMenuAccessService } from './tenant-menu-access.service';
|
||||||
|
|
||||||
/** Environment */
|
/** Environment */
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
@@ -23,6 +24,7 @@ export class HasPermissionDirective {
|
|||||||
private templateRef = inject<TemplateRef<any>>(TemplateRef);
|
private templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||||
private viewContainer = inject(ViewContainerRef);
|
private viewContainer = inject(ViewContainerRef);
|
||||||
private authenticationService = inject(AuthenticationService);
|
private authenticationService = inject(AuthenticationService);
|
||||||
|
private tenantMenuAccessService = inject(TenantMenuAccessService);
|
||||||
|
|
||||||
/** User Permissions */
|
/** User Permissions */
|
||||||
private userPermissions: any[];
|
private userPermissions: any[];
|
||||||
@@ -75,6 +77,13 @@ export class HasPermissionDirective {
|
|||||||
// Menu & Sidebar permissions are ALWAYS enforced (hide/show nav items based on role)
|
// Menu & Sidebar permissions are ALWAYS enforced (hide/show nav items based on role)
|
||||||
const isMenuPermission = permission.startsWith('MENU_') || permission.startsWith('SIDEBAR_');
|
const isMenuPermission = permission.startsWith('MENU_') || permission.startsWith('SIDEBAR_');
|
||||||
|
|
||||||
|
// The tenant's own ceiling on which menu sections it may ever show — checked before anything
|
||||||
|
// else below, ALL_FUNCTIONS included, because ALL_FUNCTIONS bypassing this is exactly the gap
|
||||||
|
// this exists to close (see TenantMenuAccessApiResource). Non-menu permissions never touch this.
|
||||||
|
if (isMenuPermission && !this.tenantMenuAccessService.isMenuAllowed(permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// For non-menu permissions: if RBAC is disabled, show everything (backward compatibility)
|
// For non-menu permissions: if RBAC is disabled, show everything (backward compatibility)
|
||||||
if (!this.rbacEnabled && !isMenuPermission) {
|
if (!this.rbacEnabled && !isMenuPermission) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { catchError, of, tap } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current tenant's own cap on which top-nav menu codes may ever show — see
|
||||||
|
* `TenantMenuAccessApiResource` on the backend for why this exists separately from a user's own
|
||||||
|
* permission list: a role carrying `ALL_FUNCTIONS` bypasses every ordinary permission check, so
|
||||||
|
* `HasPermissionDirective` needs a second, tenant-level list that check cannot see past.
|
||||||
|
*
|
||||||
|
* Loaded once per session, into a plain `Set` rather than an `Observable` the directive would have
|
||||||
|
* to subscribe to — `mifosxHasPermission`'s setter runs synchronously and once per binding, so
|
||||||
|
* `ShellComponent` loads this before the nav renders (see `ready`) and every directive instance
|
||||||
|
* afterwards just reads the already-populated set.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class TenantMenuAccessService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
/** Menu codes this tenant has explicitly turned off. Absence from this set means "shown" —
|
||||||
|
* a tenant that never configured anything (every tenant, until an operator narrows one) has
|
||||||
|
* nothing here, which is what keeps every existing deployment behaving exactly as it does today. */
|
||||||
|
private disabledCodes = new Set<string>();
|
||||||
|
|
||||||
|
/** True once the initial load has settled, success or failure. `ShellComponent` waits on this
|
||||||
|
* before rendering the nav, so a menu is never shown-then-hidden a moment later. */
|
||||||
|
private loaded = false;
|
||||||
|
|
||||||
|
isReady(): boolean {
|
||||||
|
return this.loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMenuAllowed(menuCode: string): boolean {
|
||||||
|
return !this.disabledCodes.has(menuCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetches this tenant's list once. A failed request is treated as "nothing disabled" — the same
|
||||||
|
* as a tenant that never configured this at all — so a transient network hiccup on login never
|
||||||
|
* makes the whole nav disappear. */
|
||||||
|
load() {
|
||||||
|
return this.http.get<{ menuCode: string; enabled: boolean }[]>('/tenant-menu-access').pipe(
|
||||||
|
tap((rows) => {
|
||||||
|
this.disabledCodes = new Set((rows || []).filter((r) => r.enabled === false).map((r) => r.menuCode));
|
||||||
|
this.loaded = true;
|
||||||
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
this.disabledCodes = new Set();
|
||||||
|
this.loaded = true;
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<!--
|
||||||
|
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="pma-page">
|
||||||
|
<div class="pma-shell">
|
||||||
|
<header class="pma-head">
|
||||||
|
<div class="pma-head__badge">
|
||||||
|
<fa-icon icon="shield-alt"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="pma-head__title">Bank Feature Access</h1>
|
||||||
|
<p class="pma-head__sub">
|
||||||
|
Platform-level control — caps which top-nav sections a bank can ever show, independent of any role's own
|
||||||
|
permissions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="pma-card pma-connect" [class.pma-connect--done]="tenants.length > 0">
|
||||||
|
<div class="pma-connect__field">
|
||||||
|
<label for="pma-key">Platform API Key</label>
|
||||||
|
<div class="pma-key-row">
|
||||||
|
<fa-icon icon="key" class="pma-key-ic"></fa-icon>
|
||||||
|
<input
|
||||||
|
id="pma-key"
|
||||||
|
type="password"
|
||||||
|
[(ngModel)]="apiKey"
|
||||||
|
placeholder="cbsx_••••••••••••••••••••••••"
|
||||||
|
(keyup.enter)="loadTenants()"
|
||||||
|
/>
|
||||||
|
<button type="button" class="pma-btn pma-btn--primary" [disabled]="loadingTenants" (click)="loadTenants()">
|
||||||
|
@if (loadingTenants) {
|
||||||
|
<fa-icon icon="spinner" [spin]="true"></fa-icon>
|
||||||
|
} @else if (tenants.length > 0) {
|
||||||
|
<fa-icon icon="sync-alt"></fa-icon>
|
||||||
|
} @else {
|
||||||
|
<fa-icon icon="sign-in-alt"></fa-icon>
|
||||||
|
}
|
||||||
|
{{ tenants.length > 0 ? 'Refresh' : 'Connect' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (tenants.length > 0) {
|
||||||
|
<div class="pma-status pma-status--ok">
|
||||||
|
<fa-icon icon="check-circle"></fa-icon>
|
||||||
|
Connected — {{ tenants.length }} bank{{ tenants.length !== 1 ? 's' : '' }} on this platform
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (error) {
|
||||||
|
<div class="pma-alert pma-alert--error">
|
||||||
|
<fa-icon icon="exclamation-circle"></fa-icon>
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (savedMessage) {
|
||||||
|
<div class="pma-alert pma-alert--ok">
|
||||||
|
<fa-icon icon="check-circle"></fa-icon>
|
||||||
|
<span>{{ savedMessage }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (tenants.length > 0) {
|
||||||
|
<section class="pma-card">
|
||||||
|
<div class="pma-field">
|
||||||
|
<label for="pma-tenant">Bank</label>
|
||||||
|
<select id="pma-tenant" [(ngModel)]="selectedTenant" (ngModelChange)="loadMenusForSelectedTenant()">
|
||||||
|
@for (t of tenants; track t.identifier) {
|
||||||
|
<option [value]="t.identifier">{{ t.name }} · {{ t.identifier }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (loadingMenus) {
|
||||||
|
<div class="pma-loading">
|
||||||
|
<fa-icon icon="spinner" [spin]="true"></fa-icon>
|
||||||
|
Loading {{ selectedTenantName }}'s menu list…
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!loadingMenus && rows.length > 0) {
|
||||||
|
<div class="pma-toolbar">
|
||||||
|
<div class="pma-count">
|
||||||
|
<span class="pma-count__num">{{ enabledCount }}</span>
|
||||||
|
<span class="pma-count__of">/ {{ rows.length }} shown</span>
|
||||||
|
</div>
|
||||||
|
<div class="pma-toolbar__actions">
|
||||||
|
<button type="button" class="pma-btn pma-btn--ghost" (click)="setAll(true)">
|
||||||
|
<fa-icon icon="eye"></fa-icon> Select all
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pma-btn pma-btn--ghost" (click)="setAll(false)">
|
||||||
|
<fa-icon icon="eye-slash"></fa-icon> Clear all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pma-grid">
|
||||||
|
@for (row of rows; track row.code) {
|
||||||
|
<label class="pma-tile" [class.pma-tile--on]="row.enabled">
|
||||||
|
<fa-icon [icon]="row.icon" class="pma-tile__ic"></fa-icon>
|
||||||
|
<span class="pma-tile__label">{{ row.label }}</span>
|
||||||
|
<span class="pma-switch">
|
||||||
|
<input type="checkbox" [(ngModel)]="row.enabled" />
|
||||||
|
<span class="pma-switch__track"><span class="pma-switch__thumb"></span></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pma-save-bar">
|
||||||
|
<span class="pma-save-bar__hint"
|
||||||
|
>Changes apply the moment {{ selectedTenantName }}'s users next load the app.</span
|
||||||
|
>
|
||||||
|
<button type="button" class="pma-btn pma-btn--primary pma-save" [disabled]="saving" (click)="save()">
|
||||||
|
@if (saving) {
|
||||||
|
<fa-icon icon="spinner" [spin]="true"></fa-icon>
|
||||||
|
Saving…
|
||||||
|
} @else {
|
||||||
|
<fa-icon icon="save"></fa-icon>
|
||||||
|
Save for {{ selectedTenantName }}
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Design tokens ──────────────────────────────────────────────────────────
|
||||||
|
$ink: #0f172a;
|
||||||
|
$ink-soft: #475569;
|
||||||
|
$ink-faint: #94a3b8;
|
||||||
|
$border: #e2e8f0;
|
||||||
|
$bg: #eef2f7;
|
||||||
|
$card: #fff;
|
||||||
|
$accent: #4338ca;
|
||||||
|
$accent-soft: #eef2ff;
|
||||||
|
$success: #16a34a;
|
||||||
|
$success-soft: #f0fdf4;
|
||||||
|
$danger: #dc2626;
|
||||||
|
$danger-soft: #fef2f2;
|
||||||
|
|
||||||
|
.pma-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px clamp(20px, 4vw, 64px) 80px;
|
||||||
|
background: radial-gradient(circle at 10% 0%, rgb(67 56 202 / 8%), transparent 45%), $bg;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-shell {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header ────────────────────────────────────────────────────────────────
|
||||||
|
.pma-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
flex: none;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $ink;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
box-shadow: 0 6px 16px rgb(15 23 42 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $ink;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sub {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $ink-soft;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cards ─────────────────────────────────────────────────────────────────
|
||||||
|
.pma-card {
|
||||||
|
background: $card;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connect block ─────────────────────────────────────────────────────────
|
||||||
|
.pma-connect__field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: $ink-soft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-key-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
max-width: 640px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 14px 0 38px;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
color: $ink;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-key-ic {
|
||||||
|
position: absolute;
|
||||||
|
left: 13px;
|
||||||
|
color: $ink-faint;
|
||||||
|
font-size: 13px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&--ok {
|
||||||
|
background: $success-soft;
|
||||||
|
color: $success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Alerts ────────────────────────────────────────────────────────────────
|
||||||
|
.pma-alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background: $danger-soft;
|
||||||
|
color: $danger;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--ok {
|
||||||
|
background: $success-soft;
|
||||||
|
color: $success;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Field / select ────────────────────────────────────────────────────────
|
||||||
|
.pma-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: $ink-soft;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
height: 42px;
|
||||||
|
max-width: 480px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $ink;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 32px 0;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: $ink-soft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ───────────────────────────────────────────────────────────────
|
||||||
|
.pma-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&__num {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__of {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $ink-faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-toolbar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Buttons ───────────────────────────────────────────────────────────────
|
||||||
|
.pma-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1.5px solid transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: $ink;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--ghost {
|
||||||
|
background: #fff;
|
||||||
|
border-color: $border;
|
||||||
|
color: $ink-soft;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 13px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $accent;
|
||||||
|
color: $accent;
|
||||||
|
background: $accent-soft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Menu tile grid ────────────────────────────────────────────────────────
|
||||||
|
.pma-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-tile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1.5px solid $border;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fafbfc;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--on {
|
||||||
|
background: $accent-soft;
|
||||||
|
border-color: #c7d2fe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-tile__ic {
|
||||||
|
flex: none;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $ink-faint;
|
||||||
|
|
||||||
|
.pma-tile--on & {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-tile__label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $ink;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom toggle switch — checkbox visually hidden, styled track+thumb driven by :checked.
|
||||||
|
.pma-switch {
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:checked ~ .pma-switch__track {
|
||||||
|
background: $accent;
|
||||||
|
|
||||||
|
.pma-switch__thumb {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-switch__track {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-switch__thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 25%);
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save bar ──────────────────────────────────────────────────────────────
|
||||||
|
.pma-save-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 1px solid $border;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-save-bar__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $ink-faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-save {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Responsive ────────────────────────────────────────────────────────────
|
||||||
|
@media (width <= 560px) {
|
||||||
|
.pma-key-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-save-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pma-save {
|
||||||
|
margin-left: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* 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 } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
interface MenuRow {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TenantOption {
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone platform-operator screen — deliberately outside the normal login/tenant-selector flow
|
||||||
|
* every other page in this app goes through. It talks to the backend with a hand-entered API key
|
||||||
|
* (`X-Api-Key`) instead of a Fineract session, exactly the credential `TenantMenuAccessApiResource`
|
||||||
|
* requires for its write side and accepts for its reads. Reachable at #/platform-admin/menu-access
|
||||||
|
* without logging in, on purpose: the real security boundary is the key, checked on the backend, not
|
||||||
|
* this page's URL — a bank's own super user has no way to obtain that key at all, and without it
|
||||||
|
* every call here 401s regardless of who is looking at the page.
|
||||||
|
*
|
||||||
|
* The key itself is minted once via `POST /v1/apiclients` (super-user session, one-time) and pasted
|
||||||
|
* in below; this page only remembers it in this browser's localStorage for convenience across visits,
|
||||||
|
* it is never sent anywhere but the two endpoints below.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-platform-menu-access',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
FaIconComponent
|
||||||
|
],
|
||||||
|
templateUrl: './menu-access.component.html',
|
||||||
|
styleUrl: './menu-access.component.scss'
|
||||||
|
})
|
||||||
|
export class PlatformMenuAccessComponent {
|
||||||
|
private static readonly KEY_STORAGE = 'cbsx_platform_api_key';
|
||||||
|
|
||||||
|
/** Every menu code TenantMenuAccessApiResource knows about, with the label and icon shown on this
|
||||||
|
* bank's own top nav — same icon names as shell.component.html, so a toggle here is recognisable
|
||||||
|
* as the exact item it controls. Kept in the same order as the nav's two rows. */
|
||||||
|
private static readonly ALL_MENUS: { code: string; label: string; icon: string }[] = [
|
||||||
|
{ code: 'MENU_INSTITUTION', label: 'Client Master', icon: 'users-cog' },
|
||||||
|
{ code: 'SIDEBAR_CFCFRMS', label: 'CFCFRMS', icon: 'user-shield' },
|
||||||
|
{ code: 'MENU_TASKS', label: 'Approvals', icon: 'check' },
|
||||||
|
{ code: 'MENU_CASHIER', label: 'Cashier', icon: 'money-bill' },
|
||||||
|
{ code: 'MENU_CLERK', label: 'Clerk', icon: 'id-card' },
|
||||||
|
{ code: 'MENU_BRANCH_OPS', label: 'Branch Ops', icon: 'building' },
|
||||||
|
{ code: 'MENU_CLEARING', label: 'Clearing', icon: 'exchange-alt' },
|
||||||
|
{ code: 'MENU_DEPOSITS', label: 'Deposits', icon: 'piggy-bank' },
|
||||||
|
{ code: 'MENU_ADVANCES', label: 'Advances', icon: 'hand-holding-usd' },
|
||||||
|
{ code: 'MENU_LOAN_ORIGINATION', label: 'Loan Origination', icon: 'clipboard-check' },
|
||||||
|
{ code: 'MENU_SHARE', label: 'Share', icon: 'coins' },
|
||||||
|
{ code: 'MENU_NPA', label: 'NPA', icon: 'exclamation-triangle' },
|
||||||
|
{ code: 'MENU_GENERAL_LEDGER', label: 'General Ledger', icon: 'book-open' },
|
||||||
|
{ code: 'MENU_SUB_LEDGER', label: 'Sub Ledger', icon: 'book' },
|
||||||
|
{ code: 'MENU_ACCOUNTING', label: 'Accounting', icon: 'money-bill-alt' },
|
||||||
|
{ code: 'MENU_CKYC_MGMT', label: 'CKYC', icon: 'fingerprint' },
|
||||||
|
{ code: 'MENU_INSURANCE', label: 'Insurance', icon: 'shield-alt' },
|
||||||
|
{ code: 'MENU_DIGITAL', label: 'Digital', icon: 'mobile-alt' },
|
||||||
|
{ code: 'MENU_HRM', label: 'HRM', icon: 'money-check' },
|
||||||
|
{ code: 'MENU_ADMIN', label: 'Admin', icon: 'shield-alt' },
|
||||||
|
{ code: 'MENU_RBI', label: 'RBI', icon: 'university' },
|
||||||
|
{ code: 'MENU_INVESTMENT', label: 'Investment', icon: 'chart-line' },
|
||||||
|
{ code: 'MENU_REPORTS', label: 'Reports', icon: 'chart-bar' },
|
||||||
|
{ code: 'MENU_OTHERS', label: 'Others', icon: 'folder-open' },
|
||||||
|
{ code: 'MENU_CONFIG_WIZARD', label: 'Reconciliation', icon: 'sync-alt' },
|
||||||
|
{ code: 'MENU_CHEQUEBOOK', label: 'Cheque Book', icon: 'book' },
|
||||||
|
{ code: 'MENU_LETTER_MGMT', label: 'Documents Hub', icon: 'envelope' }
|
||||||
|
];
|
||||||
|
|
||||||
|
apiKey = '';
|
||||||
|
tenants: TenantOption[] = [];
|
||||||
|
selectedTenant = '';
|
||||||
|
rows: MenuRow[] = [];
|
||||||
|
|
||||||
|
loadingTenants = false;
|
||||||
|
loadingMenus = false;
|
||||||
|
saving = false;
|
||||||
|
error: string | null = null;
|
||||||
|
savedMessage: string | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.apiKey = localStorage.getItem(PlatformMenuAccessComponent.KEY_STORAGE) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private headers(tenantForFilter?: string): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'X-Api-Key': this.apiKey,
|
||||||
|
// Any real tenant satisfies the platform's tenant-resolution filter; the target tenant for
|
||||||
|
// read/write is this header's own value, so it is set to whichever tenant is actually in play.
|
||||||
|
'Fineract-Platform-TenantId': tenantForFilter || this.selectedTenant || (this.tenants[0]?.identifier ?? 'default')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private apiUrl(path: string): string {
|
||||||
|
return `${environment.serverUrl}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
rememberKey(): void {
|
||||||
|
localStorage.setItem(PlatformMenuAccessComponent.KEY_STORAGE, this.apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTenants(): Promise<void> {
|
||||||
|
this.error = null;
|
||||||
|
this.savedMessage = null;
|
||||||
|
if (!this.apiKey.trim()) {
|
||||||
|
this.error = 'Enter the platform API key first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rememberKey();
|
||||||
|
this.loadingTenants = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(this.apiUrl('/tenant-menu-access/tenants'), { headers: this.headers('default') });
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(
|
||||||
|
res.status === 403 || res.status === 401 ? 'Invalid or inactive API key.' : `Server error (${res.status}).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.tenants = await res.json();
|
||||||
|
if (this.tenants.length > 0 && !this.selectedTenant) {
|
||||||
|
this.selectedTenant = this.tenants[0].identifier;
|
||||||
|
await this.loadMenusForSelectedTenant();
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this.error = e?.message ?? 'Could not load the tenant list.';
|
||||||
|
this.tenants = [];
|
||||||
|
} finally {
|
||||||
|
this.loadingTenants = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMenusForSelectedTenant(): Promise<void> {
|
||||||
|
this.error = null;
|
||||||
|
this.savedMessage = null;
|
||||||
|
if (!this.selectedTenant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loadingMenus = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(this.apiUrl('/tenant-menu-access'), { headers: this.headers() });
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Could not read this tenant's menu list (${res.status}).`);
|
||||||
|
}
|
||||||
|
const saved: { menuCode: string; enabled: boolean }[] = await res.json();
|
||||||
|
const enabledByCode = new Map(
|
||||||
|
saved.map((r) => [
|
||||||
|
r.menuCode,
|
||||||
|
r.enabled
|
||||||
|
])
|
||||||
|
);
|
||||||
|
// A code the tenant has never had a row saved for yet reads as enabled — matches the backend's
|
||||||
|
// own default (0541 seeds every existing tenant with everything on), so a freshly onboarded
|
||||||
|
// bank that nobody has touched here shows its real, unrestricted state, not a false "all off".
|
||||||
|
this.rows = PlatformMenuAccessComponent.ALL_MENUS.map((m) => ({
|
||||||
|
code: m.code,
|
||||||
|
label: m.label,
|
||||||
|
icon: m.icon,
|
||||||
|
enabled: enabledByCode.has(m.code) ? !!enabledByCode.get(m.code) : true
|
||||||
|
}));
|
||||||
|
} catch (e: any) {
|
||||||
|
this.error = e?.message ?? "Could not load this tenant's menu list.";
|
||||||
|
this.rows = [];
|
||||||
|
} finally {
|
||||||
|
this.loadingMenus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(): Promise<void> {
|
||||||
|
this.error = null;
|
||||||
|
this.savedMessage = null;
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
const body = this.rows.map((r) => ({ menuCode: r.code, enabled: r.enabled }));
|
||||||
|
const res = await fetch(this.apiUrl('/tenant-menu-access'), {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { ...this.headers(), 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Save failed (${res.status}).`);
|
||||||
|
}
|
||||||
|
this.savedMessage = `Saved for "${this.selectedTenant}".`;
|
||||||
|
} catch (e: any) {
|
||||||
|
this.error = e?.message ?? 'Could not save.';
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAll(enabled: boolean): void {
|
||||||
|
this.rows = this.rows.map((r) => ({ ...r, enabled }));
|
||||||
|
}
|
||||||
|
|
||||||
|
get enabledCount(): number {
|
||||||
|
return this.rows.filter((r) => r.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedTenantName(): string {
|
||||||
|
return this.tenants.find((t) => t.identifier === this.selectedTenant)?.name ?? this.selectedTenant;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
// Tenants the login screen offers, and which one it starts on. Every identifier here has to exist
|
// Tenants the login screen offers, and which one it starts on. Every identifier here has to exist
|
||||||
// in the tenants table of the tenant-store database, or the platform rejects the request as an
|
// in the tenants table of the tenant-store database, or the platform rejects the request as an
|
||||||
// unknown tenant — 'socity_bank' was listed with no such row behind it.
|
// unknown tenant.
|
||||||
window["env"]["fineractPlatformTenantId"] = 'default';
|
window["env"]["fineractPlatformTenantId"] = 'default';
|
||||||
window["env"]["fineractPlatformTenantIds"] = 'default,abc_bank';
|
window["env"]["fineractPlatformTenantIds"] = 'default,abc_bank,socity_bank';
|
||||||
|
|
||||||
window['env']['tenantLogoUrl'] = '';
|
window['env']['tenantLogoUrl'] = '';
|
||||||
window['env']['tenantLogoUrlDark'] = '';
|
window['env']['tenantLogoUrlDark'] = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user