feat(cashier,organization): denomination-aware vault UI, Bank/Branch Profile screens, popup disbursement review
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s

Branch Day open/close now captures a denomination-wise count (carried
forward automatically to the next day), and shows an auto-fetched EOD
summary at close time -- vault + every cashier's position, a branch-total
reconciliation against the book figure, and each cashier's
reconciliation/settlement status and transaction count -- so closing the day
no longer means separately checking Vault Position and every cashier's own
screen by hand. New Vault Position screen (branch-ops menu) mirrors Cashier
Position for the teller/vault. Cash Transfer now shows the vault's live
per-denomination availability while counting a handout, with a client-side
warning before it can be over-counted.

New Organization screens for Bank Profile, Branch Profile and Clearing GL
Configuration, wired into the Organization landing page and the
previously-dead "Bank Profile" admin menu button.

Cashier Transaction Entry: TYPE is now Receipt/Payment only (cash-only TXN
TP.) on the cashier side, with no change to the clerk dashboard's own
Withdrawal Slip/Cheque/Disbursement/Enter Bank Transfer/DD flow beyond
adding a new Fund Transfer type alongside Disbursement for a GL credit that
isn't a loan payout. Neither TYPE nor TXN TP. offers a blank placeholder
option anymore -- a new row starts on a real default instead. Footer no
longer polls the business-date config for cashier/clerk roles, who never
have permission to read it.

Disbursement & Transfer Requests: "Review" now opens the review screen as a
popup instead of navigating to a separate route, refreshing the list on
close; the review screen recognises the new Fund Transfer request type
too, and its non-functional "+ Disbursement Review" tab-styled label is now
a plain heading.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 14:40:13 +05:30
co-authored by Claude Sonnet 5
parent ce840d73d7
commit da1f57c3b6
32 changed files with 3006 additions and 136 deletions
@@ -0,0 +1,163 @@
<!--
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="bp-container">
<div class="bp-header">
<button mat-stroked-button [routerLink]="['/organization']" class="bp-back">
<i class="fas fa-arrow-left"></i> Organization
</button>
<h2 class="bp-header__title">Branch Profile</h2>
</div>
@if (errorMsg) {
<div class="bp-alert bp-alert--error">{{ errorMsg }}</div>
}
@if (successMsg) {
<div class="bp-alert bp-alert--success">{{ successMsg }}</div>
}
<div class="bp-card">
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-code-branch"></i>
<h3>Select Branch</h3>
</div>
<div class="bp-body">
@if (loadingOffices) {
<span class="bp-muted">Loading branches…</span>
} @else {
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Branch</mat-label>
<mat-select [value]="selectedOfficeId" (selectionChange)="selectOffice($event.value)">
@for (o of offices; track o.officeId) {
<mat-option [value]="o.officeId">{{ officeLabel(o) }}</mat-option>
}
</mat-select>
</mat-form-field>
}
</div>
</div>
@if (loadingProfile) {
<div class="bp-loading">Loading…</div>
}
@if (selectedOfficeId && !loadingProfile) {
<form [formGroup]="form" (ngSubmit)="submit()">
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-id-card"></i>
<h3>Branch Details</h3>
</div>
<div class="bp-body">
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>Branch Code</mat-label>
<input matInput formControlName="branchCode" placeholder="e.g. BR001" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>Branch Type</mat-label>
<mat-select formControlName="branchType">
@for (t of branchTypes; track t.value) {
<mat-option [value]="t.value">{{ t.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field">
<mat-label>Address</mat-label>
<textarea matInput formControlName="address" rows="2"></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>Contact Phone</mat-label>
<input matInput formControlName="contactPhone" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>Contact Email</mat-label>
<input matInput type="email" formControlName="contactEmail" />
</mat-form-field>
</div>
</div>
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-landmark"></i>
<h3>Branch Identifiers</h3>
</div>
<div class="bp-body">
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>IFSC Code</mat-label>
<input matInput formControlName="ifscCode" placeholder="e.g. SUCB0000123" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>MICR Code</mat-label>
<input matInput formControlName="micrCode" />
</mat-form-field>
</div>
<p class="bp-note">
SWIFT/BIC is set once for the whole bank, not per branch — see
<a [routerLink]="['/organization/institution-profile']">Bank Profile</a>.
</p>
</div>
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-map-marked-alt"></i>
<h3>Location (optional)</h3>
</div>
<div class="bp-body">
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>Latitude</mat-label>
<input matInput type="number" step="0.000001" formControlName="latitude" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--half">
<mat-label>Longitude</mat-label>
<input matInput type="number" step="0.000001" formControlName="longitude" />
</mat-form-field>
</div>
</div>
<div class="bp-section">
<div class="bp-section__header">
<i class="fas fa-clock"></i>
<h3>Working Hours</h3>
</div>
<div class="bp-body">
<mat-form-field appearance="outline" class="bp-field bp-field--third">
<mat-label>Opens At</mat-label>
<input matInput type="time" formControlName="workingHoursStart" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--third">
<mat-label>Closes At</mat-label>
<input matInput type="time" formControlName="workingHoursEnd" />
</mat-form-field>
<mat-form-field appearance="outline" class="bp-field bp-field--third">
<mat-label>Working Days</mat-label>
<input matInput formControlName="workingDays" placeholder="e.g. Mon-Sat" />
</mat-form-field>
</div>
</div>
@if (lastUpdated) {
<div class="bp-lastupdated">Last updated by {{ lastUpdated.by }} · {{ lastUpdated.at | dateFormat }}</div>
}
<div class="bp-actions">
<button
type="submit"
mat-flat-button
color="primary"
[disabled]="form.pristine || saving"
*mifosxHasPermission="'UPDATE_BRANCHPROFILE'"
class="bp-actions__submit"
>
<i class="fas fa-check"></i> Save
</button>
</div>
</form>
}
</div>
</div>
@@ -0,0 +1,175 @@
/**
* 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/.
*/
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-900: #202124;
.bp-container {
max-width: 760px;
margin: 0 auto;
padding: 20px 16px;
}
.bp-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
}
}
.bp-back {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 32px !important;
border-color: $border !important;
color: $gray-500 !important;
i {
margin-right: 6px;
}
&:hover {
color: $primary !important;
border-color: $primary !important;
}
}
.bp-alert {
padding: 10px 14px;
margin-bottom: 14px;
border-radius: 8px;
font-size: 13px;
&--error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
&--success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #14532d;
}
}
.bp-muted {
color: $gray-500;
font-size: 13px;
}
.bp-loading {
padding: 24px;
text-align: center;
color: $gray-500;
}
.bp-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
.bp-section {
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.bp-body {
padding: 20px 24px;
display: flex;
flex-wrap: wrap;
gap: 0 16px;
}
.bp-field {
width: 100%;
margin-bottom: 4px;
&--half {
width: calc(50% - 8px);
}
&--third {
width: calc(33.33% - 11px);
}
}
.bp-note {
margin: 0 24px 16px;
padding: 8px 12px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 6px;
font-size: 11px;
color: #1e40af;
a {
color: $primary;
font-weight: 600;
}
}
.bp-lastupdated {
padding: 0 24px 10px;
font-size: 11px;
color: $gray-500;
}
.bp-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px;
border-top: 1px solid $gray-200;
background: $gray-50;
&__submit {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
i {
margin-right: 6px;
font-size: 12px;
}
}
}
@@ -0,0 +1,151 @@
/**
* 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, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { catchError, of } from 'rxjs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { OrganizationService } from '../organization.service';
const BRANCH_TYPES = [
{ value: 'HEAD_OFFICE', label: 'Head Office' },
{ value: 'BRANCH', label: 'Branch' },
{ value: 'REGIONAL_OFFICE', label: 'Regional Office' },
{ value: 'ZONAL_OFFICE', label: 'Zonal Office' }
];
/**
* The real-world operational fields behind a branch — code, address, contact, type and working
* hours — kept as their own screen on top of the stock Manage Offices flow, since m_office (the
* core Fineract entity) was deliberately left untouched. Pick an office, fill in what real-world
* UCB branch setup expects, save.
*/
@Component({
selector: 'mifosx-branch-profile',
templateUrl: './branch-profile.component.html',
styleUrls: ['./branch-profile.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class BranchProfileComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private organizationService = inject(OrganizationService);
branchTypes = BRANCH_TYPES;
offices: any[] = [];
selectedOfficeId: number | null = null;
form: UntypedFormGroup;
loadingOffices = true;
loadingProfile = false;
saving = false;
errorMsg = '';
successMsg = '';
lastUpdated: any = null;
ngOnInit(): void {
this.form = this.formBuilder.group({
branchCode: [''],
branchType: [null],
address: [''],
contactPhone: [''],
contactEmail: [''],
workingHoursStart: [''],
workingHoursEnd: [''],
workingDays: [''],
ifscCode: [''],
micrCode: [''],
latitude: [null],
longitude: [null]
});
this.organizationService
.getBranchProfiles()
.pipe(
catchError((err: any) => {
this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not load branches.';
return of([]);
})
)
.subscribe((res: any[]) => {
this.loadingOffices = false;
this.offices = res || [];
if (this.offices.length === 1) {
this.selectOffice(this.offices[0].officeId);
}
});
}
selectOffice(officeId: number): void {
this.selectedOfficeId = officeId;
this.loadingProfile = true;
this.errorMsg = '';
this.successMsg = '';
this.organizationService
.getBranchProfile(officeId)
.pipe(
catchError((err: any) => {
this.loadingProfile = false;
this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not load this branch.';
return of(null);
})
)
.subscribe((res: any) => {
this.loadingProfile = false;
if (!res) return;
this.form.patchValue({
branchCode: res.branchCode ?? '',
branchType: res.branchType ?? null,
address: res.address ?? '',
contactPhone: res.contactPhone ?? '',
contactEmail: res.contactEmail ?? '',
workingHoursStart: res.workingHoursStart ?? '',
workingHoursEnd: res.workingHoursEnd ?? '',
workingDays: res.workingDays ?? '',
ifscCode: res.ifscCode ?? '',
micrCode: res.micrCode ?? '',
latitude: res.latitude ?? null,
longitude: res.longitude ?? null
});
this.lastUpdated = res.updatedByUsername ? { by: res.updatedByUsername, at: res.updatedAt } : null;
this.form.markAsPristine();
// Reflect the just-loaded office in the list so the selector stays accurate without a reload.
const idx = this.offices.findIndex((o) => o.officeId === officeId);
if (idx >= 0) this.offices[idx] = res;
});
}
officeLabel(o: any): string {
return o.branchCode ? `${o.officeName} (${o.branchCode})` : o.officeName;
}
submit(): void {
if (this.saving || !this.selectedOfficeId) return;
this.saving = true;
this.errorMsg = '';
this.successMsg = '';
this.organizationService
.updateBranchProfile(this.selectedOfficeId, this.form.value)
.pipe(
catchError((err: any) => {
this.saving = false;
this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not save this branch.';
return of(null);
})
)
.subscribe((res: any) => {
this.saving = false;
if (!res) return;
this.successMsg = 'Branch profile saved.';
this.form.markAsPristine();
this.lastUpdated = res.updatedByUsername ? { by: res.updatedByUsername, at: res.updatedAt } : null;
const idx = this.offices.findIndex((o) => o.officeId === this.selectedOfficeId);
if (idx >= 0) this.offices[idx] = res;
});
}
}
@@ -0,0 +1,70 @@
<!--
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="cgc-container">
<div class="cgc-header">
<button mat-stroked-button [routerLink]="['/organization']" class="cgc-back">
<i class="fas fa-arrow-left"></i> Organization
</button>
<h2 class="cgc-header__title">Clearing GL Configuration</h2>
</div>
<p class="cgc-sub">
Which GL account backs each clearing role. An unmapped role posts nowhere — map it here before that clearing channel
goes live.
</p>
@if (errorMsg) {
<div class="cgc-alert cgc-alert--error">{{ errorMsg }}</div>
}
@if (successMsg) {
<div class="cgc-alert cgc-alert--success">{{ successMsg }}</div>
}
@if (loading) {
<div class="cgc-loading">Loading…</div>
} @else {
<div class="cgc-card">
<table class="cgc-table">
<thead>
<tr>
<th>Clearing Role</th>
<th>Description</th>
<th>Mapped GL Account</th>
</tr>
</thead>
<tbody>
@for (row of rows; track row.configKey) {
<tr>
<td>
<span class="cgc-key">{{ row.configKey }}</span>
</td>
<td class="cgc-desc">{{ row.description }}</td>
<td>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="cgc-select">
<mat-select
[value]="row.glAccountId"
[disabled]="savingKey === row.configKey"
(selectionChange)="onSelect(row, $event.value)"
>
<mat-option [value]="null">— Not mapped —</mat-option>
@for (o of glAccountOptions; track o.id) {
<mat-option [value]="o.id">{{ o.glCode }} · {{ o.name }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (!row.glAccountId) {
<span class="cgc-unmapped">Unmapped</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
@@ -0,0 +1,137 @@
/**
* 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/.
*/
$primary: #1353a0;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-900: #202124;
.cgc-container {
max-width: 900px;
margin: 0 auto;
padding: 20px 16px;
}
.cgc-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 6px;
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
}
}
.cgc-sub {
margin: 0 0 16px;
font-size: 12px;
color: $gray-500;
}
.cgc-back {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 32px !important;
border-color: $border !important;
color: $gray-500 !important;
i {
margin-right: 6px;
}
&:hover {
color: $primary !important;
border-color: $primary !important;
}
}
.cgc-alert {
padding: 10px 14px;
margin-bottom: 14px;
border-radius: 8px;
font-size: 13px;
&--error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
&--success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #14532d;
}
}
.cgc-loading {
padding: 24px;
text-align: center;
color: $gray-500;
}
.cgc-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
.cgc-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th {
text-align: left;
padding: 10px 16px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: $gray-500;
}
td {
padding: 10px 16px;
border-bottom: 1px solid $gray-200;
vertical-align: middle;
}
tr:last-child td {
border-bottom: none;
}
}
.cgc-key {
font-family: monospace;
font-weight: 700;
color: $primary;
}
.cgc-desc {
color: #475569;
}
.cgc-select {
min-width: 260px;
}
.cgc-unmapped {
margin-left: 8px;
font-size: 11px;
font-weight: 700;
color: #b45309;
}
@@ -0,0 +1,75 @@
/**
* 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, OnInit, inject } from '@angular/core';
import { catchError, forkJoin, of } from 'rxjs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { OrganizationService } from '../organization.service';
/**
* Which GL account backs each clearing role (CTS inward/outward suspense, NACH suspense, RTGS
* settlement, ...). The roles themselves are fixed; this only changes which GL account each one
* points at.
*/
@Component({
selector: 'mifosx-clearing-gl-config',
templateUrl: './clearing-gl-config.component.html',
styleUrls: ['./clearing-gl-config.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class ClearingGlConfigComponent implements OnInit {
private organizationService = inject(OrganizationService);
rows: any[] = [];
glAccountOptions: any[] = [];
loading = true;
savingKey: string | null = null;
errorMsg = '';
successMsg = '';
ngOnInit(): void {
this.load();
}
load(): void {
this.loading = true;
this.errorMsg = '';
forkJoin({
rows: this.organizationService.getClearingGlConfig().pipe(catchError(() => of([]))),
template: this.organizationService.getClearingGlConfigTemplate().pipe(catchError(() => of(null)))
}).subscribe(({ rows, template }: any) => {
this.loading = false;
this.rows = rows || [];
this.glAccountOptions = template?.glAccountOptions || [];
if (!rows || rows.length === 0) {
this.errorMsg = 'Could not load clearing GL configuration.';
}
});
}
onSelect(row: any, glAccountId: number | null): void {
this.savingKey = row.configKey;
this.errorMsg = '';
this.successMsg = '';
this.organizationService
.updateClearingGlConfig(row.configKey, glAccountId)
.pipe(
catchError((err: any) => {
this.savingKey = null;
this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not save this mapping.';
return of(null);
})
)
.subscribe((res: any[]) => {
this.savingKey = null;
if (!res) return;
this.rows = res;
this.successMsg = `${row.configKey} updated.`;
});
}
}
@@ -0,0 +1,392 @@
<!--
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="ip-container">
<div class="ip-header">
<button mat-stroked-button [routerLink]="['/organization']" class="ip-back">
<i class="fas fa-arrow-left"></i> Organization
</button>
<h2 class="ip-header__title">Bank Profile</h2>
</div>
@if (errorMsg) {
<div class="ip-alert ip-alert--error">{{ errorMsg }}</div>
}
@if (successMsg) {
<div class="ip-alert ip-alert--success">{{ successMsg }}</div>
}
@if (loading) {
<div class="ip-loading">Loading…</div>
} @else {
<div class="ip-card">
<form [formGroup]="form" (ngSubmit)="submit()">
<!-- 1. Basic Information -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-university"></i>
<h3>Basic Information</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Bank / Institution Name</mat-label>
<input matInput formControlName="bankName" placeholder="e.g. Socity Urban Co-operative Bank" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Short Name / Abbreviation</mat-label>
<input matInput formControlName="shortName" placeholder="e.g. SUCB" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Legal / Registered Name</mat-label>
<input matInput formControlName="legalName" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Bank Code</mat-label>
<input matInput formControlName="bankCode" placeholder="e.g. SUCB001" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Institution Code</mat-label>
<input matInput formControlName="institutionCode" placeholder="e.g. INST001" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Institution Type</mat-label>
<mat-select formControlName="bankType">
@for (t of bankTypes; track t.value) {
<mat-option [value]="t.value">{{ t.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Ownership Type</mat-label>
<mat-select formControlName="ownershipType">
@for (o of ownershipTypes; track o.value) {
<mat-option [value]="o.value">{{ o.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Parent Institution (if any)</mat-label>
<input matInput formControlName="parentInstitutionName" placeholder="Leave blank if not applicable" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Established Date</mat-label>
<input matInput [matDatepicker]="estPicker" formControlName="establishedDate" />
<mat-datepicker-toggle matSuffix [for]="estPicker"></mat-datepicker-toggle>
<mat-datepicker #estPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>Status</mat-label>
<mat-select formControlName="status">
@for (s of statuses; track s.value) {
<mat-option [value]="s.value">{{ s.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>Country</mat-label>
<input matInput formControlName="country" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>Currency</mat-label>
<input matInput [value]="currencyCode" readonly disabled />
<mat-hint>Set once for the whole system, not editable here</mat-hint>
</mat-form-field>
</div>
</div>
<!-- 2. Regulatory Information -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-gavel"></i>
<h3>Regulatory Information</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Regulatory Authority</mat-label>
<input
matInput
formControlName="regulatoryAuthority"
placeholder="e.g. RBI / Registrar of Cooperative Societies"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Registration Date</mat-label>
<input matInput [matDatepicker]="regPicker" formControlName="registrationDate" />
<mat-datepicker-toggle matSuffix [for]="regPicker"></mat-datepicker-toggle>
<mat-datepicker #regPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Registration Number</mat-label>
<input matInput formControlName="registrationNumber" placeholder="RCS registration no." />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>License Number</mat-label>
<input matInput formControlName="licenseNumber" placeholder="RBI licence no." />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>License Status</mat-label>
<mat-select formControlName="licenseStatus">
@for (l of licenseStatuses; track l.value) {
<mat-option [value]="l.value">{{ l.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>License Expiry Date</mat-label>
<input matInput [matDatepicker]="licExpPicker" formControlName="licenseExpiryDate" />
<mat-datepicker-toggle matSuffix [for]="licExpPicker"></mat-datepicker-toggle>
<mat-datepicker #licExpPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>PAN</mat-label>
<input matInput formControlName="pan" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>TAN</mat-label>
<input matInput formControlName="tan" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>GSTIN</mat-label>
<input matInput formControlName="gstin" />
</mat-form-field>
</div>
</div>
<!-- 3. Address -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-map-marker-alt"></i>
<h3>Registered Office Address</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Address Line 1</mat-label>
<input matInput formControlName="addressLine1" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Address Line 2</mat-label>
<input matInput formControlName="addressLine2" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>City</mat-label>
<input matInput formControlName="city" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>District</mat-label>
<input matInput formControlName="district" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>State</mat-label>
<input matInput formControlName="state" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--quarter">
<mat-label>PIN Code</mat-label>
<input matInput formControlName="pincode" />
</mat-form-field>
</div>
</div>
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-building"></i>
<h3>Head Office / Corporate Address</h3>
</div>
<div class="ip-body">
<mat-checkbox formControlName="headOfficeSameAsRegistered" class="ip-checkbox">
Same as Registered Office Address
</mat-checkbox>
@if (!form.value.headOfficeSameAsRegistered) {
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Head Office Address</mat-label>
<textarea matInput formControlName="headOfficeAddress" rows="2"></textarea>
</mat-form-field>
}
</div>
</div>
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-envelope-open-text"></i>
<h3>Communication Address</h3>
</div>
<div class="ip-body">
<mat-checkbox formControlName="communicationSameAsRegistered" class="ip-checkbox">
Same as Registered Office Address
</mat-checkbox>
@if (!form.value.communicationSameAsRegistered) {
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Communication Address</mat-label>
<textarea matInput formControlName="communicationAddress" rows="2"></textarea>
</mat-form-field>
}
</div>
</div>
<!-- 4. Contact -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-address-card"></i>
<h3>Contact Details</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Official Email</mat-label>
<input matInput type="email" formControlName="contactEmail" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Phone Number</mat-label>
<input matInput formControlName="contactPhone" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Customer Care Number</mat-label>
<input matInput formControlName="customerCareNumber" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Grievance / Support Email</mat-label>
<input matInput type="email" formControlName="grievanceEmail" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Website</mat-label>
<input matInput formControlName="website" placeholder="https://" />
</mat-form-field>
</div>
</div>
<!-- 5. Banking Configuration (reference only — not stored here) -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-cogs"></i>
<h3>Banking Configuration</h3>
</div>
<div class="ip-body">
<p class="ip-refnote">
Business date, financial year, time zone, working days and the holiday calendar are configured on their
own screens rather than duplicated here, so there is exactly one place each is set:
</p>
<div class="ip-reflinks">
<a [routerLink]="['/organization/working-days']" class="ip-reflink"
><i class="fas fa-calendar-alt"></i> Working Days</a
>
<a [routerLink]="['/bank-calendar']" class="ip-reflink"
><i class="fas fa-calendar-check"></i> Holiday Calendar</a
>
<a [routerLink]="['/accounting/closing-entries']" class="ip-reflink"
><i class="fas fa-calendar-day"></i> Financial Year / Closing Entries</a
>
<a [routerLink]="['/branch-ops/day-register']" class="ip-reflink"
><i class="fas fa-clock"></i> Business Date (Day Register)</a
>
</div>
</div>
</div>
<!-- 6. Banking Identifiers -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-landmark"></i>
<h3>Banking Identifiers</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>SWIFT / BIC</mat-label>
<input matInput formControlName="swiftBic" placeholder="If applicable" />
</mat-form-field>
</div>
<p class="ip-note">
IFSC and MICR are branch-level identifiers, not institution-level — a bank with multiple branches has a
different IFSC/MICR per branch, so those are set on each branch's own
<a [routerLink]="['/organization/branch-profile']">Branch Profile</a> instead.
</p>
</div>
<!-- 7. Branding / Documents -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-file-invoice"></i>
<h3>Document Headers</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Statement Header Name</mat-label>
<input matInput formControlName="statementHeaderName" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Passbook Header Name</mat-label>
<input matInput formControlName="passbookHeaderName" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--third">
<mat-label>Receipt Header Name</mat-label>
<input matInput formControlName="receiptHeaderName" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field">
<mat-label>Terms &amp; Conditions</mat-label>
<textarea matInput formControlName="termsAndConditions" rows="4"></textarea>
</mat-form-field>
</div>
</div>
<!-- 8. Authorized Representative -->
<div class="ip-section">
<div class="ip-section__header">
<i class="fas fa-user-tie"></i>
<h3>Authorized Representative</h3>
</div>
<div class="ip-body">
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Name</mat-label>
<input matInput formControlName="authorizedPersonName" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Designation</mat-label>
<input matInput formControlName="authorizedPersonDesignation" placeholder="e.g. Managing Director" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Contact Number</mat-label>
<input matInput formControlName="authorizedPersonContact" />
</mat-form-field>
<mat-form-field appearance="outline" class="ip-field ip-field--half">
<mat-label>Official Email</mat-label>
<input matInput type="email" formControlName="authorizedPersonEmail" />
</mat-form-field>
</div>
</div>
<!-- 9. Audit -->
@if (audit) {
<div class="ip-audit">
@if (audit.createdBy) {
<span>Created by {{ audit.createdBy }} · {{ audit.createdAt | dateFormat }}</span>
}
@if (audit.updatedBy) {
<span>Last updated by {{ audit.updatedBy }} · {{ audit.updatedAt | dateFormat }}</span>
}
@if (audit.activationDate) {
<span>Activated {{ audit.activationDate }}</span>
}
@if (audit.deactivationDate) {
<span>Deactivated {{ audit.deactivationDate }}</span>
}
</div>
}
<div class="ip-actions">
<button
type="submit"
mat-flat-button
color="primary"
[disabled]="form.pristine || saving"
*mifosxHasPermission="'UPDATE_INSTITUTIONPROFILE'"
class="ip-actions__submit"
>
<i class="fas fa-check"></i> Save
</button>
</div>
</form>
</div>
}
</div>
@@ -0,0 +1,226 @@
/**
* 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/.
*/
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-700: #5f6368;
$gray-900: #202124;
.ip-container {
max-width: 760px;
margin: 0 auto;
padding: 20px 16px;
}
.ip-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
}
}
.ip-back {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 32px !important;
border-color: $border !important;
color: $gray-500 !important;
i {
margin-right: 6px;
}
&:hover {
color: $primary !important;
border-color: $primary !important;
}
}
.ip-alert {
padding: 10px 14px;
margin-bottom: 14px;
border-radius: 8px;
font-size: 13px;
&--error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
&--success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #14532d;
}
}
.ip-loading {
padding: 24px;
text-align: center;
color: $gray-500;
}
.ip-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
.ip-section {
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
}
.ip-body {
padding: 20px 24px;
display: flex;
flex-wrap: wrap;
gap: 0 16px;
}
.ip-field {
width: 100%;
margin-bottom: 4px;
&--half {
width: calc(50% - 8px);
}
&--third {
width: calc(33.33% - 11px);
}
&--quarter {
width: calc(25% - 12px);
}
}
.ip-note {
margin: 0 24px 16px;
padding: 8px 12px;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 6px;
font-size: 11px;
color: #92400e;
a {
color: $primary;
font-weight: 600;
}
}
.ip-checkbox {
display: block;
margin-bottom: 8px;
font-size: 13px;
}
.ip-refnote {
margin: 0 0 10px;
font-size: 12px;
color: #64748b;
}
.ip-reflinks {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ip-reflink {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #fff;
border: 1px solid $border;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
color: $primary;
text-decoration: none;
&:hover {
background: #eff6ff;
border-color: $primary;
}
i {
font-size: 11px;
color: $accent;
}
}
.ip-lastupdated {
padding: 0 24px 10px;
font-size: 11px;
color: $gray-500;
}
.ip-audit {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
padding: 8px 24px 10px;
font-size: 11px;
color: $gray-500;
}
.ip-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px;
border-top: 1px solid $gray-200;
background: $gray-50;
&__submit {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
i {
margin-right: 6px;
font-size: 12px;
}
}
}
@@ -0,0 +1,208 @@
/**
* 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, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { catchError, of } from 'rxjs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { OrganizationService } from '../organization.service';
import { SettingsService } from 'app/settings/settings.service';
const BANK_TYPES = [
{ value: 'COOPERATIVE', label: 'Co-operative Bank' },
{ value: 'COMMERCIAL', label: 'Commercial Bank' },
{ value: 'RRB', label: 'Regional Rural Bank' },
{ value: 'NBFC', label: 'NBFC' },
{ value: 'OTHER', label: 'Other' }
];
const OWNERSHIP_TYPES = [
{ value: 'GOVERNMENT', label: 'Government' },
{ value: 'PRIVATE', label: 'Private' },
{ value: 'COOPERATIVE', label: 'Cooperative' },
{ value: 'OTHER', label: 'Other' }
];
const LICENSE_STATUSES = [
{ value: 'ACTIVE', label: 'Active' },
{ value: 'EXPIRED', label: 'Expired' },
{ value: 'SUSPENDED', label: 'Suspended' },
{ value: 'NOT_APPLICABLE', label: 'Not Applicable' }
];
const STATUSES = [
{ value: 'ACTIVE', label: 'Active' },
{ value: 'INACTIVE', label: 'Inactive' }
];
/**
* The bank's own "Bank Master" profile — identity/classification, regulatory details, address
* (registered, with optional head-office/communication overrides), contact, the institution-level
* banking identifier (SWIFT/BIC), document header names and terms, the authorised representative,
* and audit fields. A singleton screen: there is only ever one institution running this tenant.
*
* IFSC/MICR are deliberately NOT here — those identify a specific branch, not the institution, and
* live on Branch Profile instead. Business Date, Financial Year, Time Zone, Working Days and Holiday
* Calendar are likewise not stored here — they already exist as their own configuration, and this
* screen only links to them (see the Operational Configuration section) instead of holding a second,
* driftable copy.
*/
@Component({
selector: 'mifosx-institution-profile',
templateUrl: './institution-profile.component.html',
styleUrls: ['./institution-profile.component.scss'],
imports: [...STANDALONE_SHARED_IMPORTS]
})
export class InstitutionProfileComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private organizationService = inject(OrganizationService);
private settingsService = inject(SettingsService);
bankTypes = BANK_TYPES;
ownershipTypes = OWNERSHIP_TYPES;
licenseStatuses = LICENSE_STATUSES;
statuses = STATUSES;
form: UntypedFormGroup;
loading = true;
saving = false;
errorMsg = '';
successMsg = '';
currencyCode = '';
audit: any = null;
private static readonly DATE_FIELDS = [
'establishedDate',
'registrationDate',
'licenseExpiryDate'
];
ngOnInit(): void {
this.form = this.formBuilder.group({
bankName: [''],
legalName: [''],
shortName: [''],
bankCode: [''],
institutionCode: [''],
bankType: [null],
ownershipType: [null],
parentInstitutionName: [''],
establishedDate: [null],
status: ['ACTIVE'],
country: ['India'],
registrationNumber: [''],
licenseNumber: [''],
regulatoryAuthority: [''],
registrationDate: [null],
licenseStatus: [null],
licenseExpiryDate: [null],
pan: [''],
tan: [''],
gstin: [''],
addressLine1: [''],
addressLine2: [''],
city: [''],
district: [''],
state: [''],
pincode: [''],
headOfficeSameAsRegistered: [true],
headOfficeAddress: [''],
communicationSameAsRegistered: [true],
communicationAddress: [''],
contactPhone: [''],
contactEmail: [''],
customerCareNumber: [''],
grievanceEmail: [''],
website: [''],
swiftBic: [''],
statementHeaderName: [''],
passbookHeaderName: [''],
receiptHeaderName: [''],
termsAndConditions: [''],
authorizedPersonName: [''],
authorizedPersonDesignation: [''],
authorizedPersonContact: [''],
authorizedPersonEmail: ['']
});
this.load();
}
load(): void {
this.loading = true;
this.errorMsg = '';
this.organizationService
.getInstitutionProfile()
.pipe(
catchError((err: any) => {
this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not load the bank profile.';
return of(null);
})
)
.subscribe((res: any) => {
this.loading = false;
if (!res) return;
this.applyResult(res);
this.form.markAsPristine();
});
}
submit(): void {
if (this.saving) return;
this.saving = true;
this.errorMsg = '';
this.successMsg = '';
const raw = this.form.value;
const payload: any = { ...raw, locale: this.settingsService.language.code };
for (const field of InstitutionProfileComponent.DATE_FIELDS) {
payload[field] = raw[field] ? this.formatDate(raw[field]) : null;
}
this.organizationService
.updateInstitutionProfile(payload)
.pipe(
catchError((err: any) => {
this.saving = false;
this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not save the bank profile.';
return of(null);
})
)
.subscribe((res: any) => {
this.saving = false;
if (!res) return;
this.successMsg = 'Bank profile saved.';
this.applyResult(res);
this.form.markAsPristine();
});
}
private applyResult(res: any): void {
const patch: any = { ...res };
for (const field of InstitutionProfileComponent.DATE_FIELDS) {
patch[field] = res[field] ? new Date(res[field]) : null;
}
this.form.patchValue(patch);
this.currencyCode = res.currencyCode ?? '';
this.audit = {
createdBy: res.createdByUsername,
createdAt: res.createdAt,
updatedBy: res.updatedByUsername,
updatedAt: res.updatedAt,
activationDate: res.activationDate,
deactivationDate: res.deactivationDate
};
}
private formatDate(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
}
@@ -237,6 +237,30 @@ const routes: Routes = [
}
]
},
{
// Bank's own profile — name, legal name, code, registration, address/contact. A
// singleton, unlike every other screen under Organization, so it has no create/list step.
path: 'institution-profile',
loadComponent: () =>
import('./institution-profile/institution-profile.component').then((m) => m.InstitutionProfileComponent),
data: { title: 'Bank Profile', breadcrumb: 'Bank Profile' }
},
{
// The real-world fields (code, address, contact, type, working hours) behind a branch —
// kept as its own screen rather than folded into Manage Offices, since the underlying
// m_office record is a stock Fineract entity this CBS deliberately left untouched.
path: 'branch-profile',
loadComponent: () =>
import('./branch-profile/branch-profile.component').then((m) => m.BranchProfileComponent),
data: { title: 'Branch Profile', breadcrumb: 'Branch Profile' }
},
{
// Which GL account backs each clearing suspense/settlement role (CTS, NACH, RTGS, ...).
path: 'clearing-gl-config',
loadComponent: () =>
import('./clearing-gl-config/clearing-gl-config.component').then((m) => m.ClearingGlConfigComponent),
data: { title: 'Clearing GL Configuration', breadcrumb: 'Clearing GL Configuration' }
},
{
path: 'employees',
data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' },
@@ -34,6 +34,48 @@
</a>
</div>
<!-- Bank Profile -->
<div #institutionProfile>
<a class="org-card" [routerLink]="['institution-profile']" *mifosxHasPermission="'READ_INSTITUTIONPROFILE'">
<div class="org-card__icon org-card__icon--blue">
<i class="fas fa-university"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Bank Profile</h3>
<p class="org-card__desc">Bank name, legal name, code, registration and contact details</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<!-- Branch Profile -->
<div #branchProfile>
<a class="org-card" [routerLink]="['branch-profile']" *mifosxHasPermission="'READ_BRANCHPROFILE'">
<div class="org-card__icon org-card__icon--blue">
<i class="fas fa-code-branch"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Branch Profile</h3>
<p class="org-card__desc">Branch code, address, contact, type and working hours per office</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<!-- Clearing GL Configuration -->
<div #clearingGlConfig>
<a class="org-card" [routerLink]="['clearing-gl-config']" *mifosxHasPermission="'READ_CLEARINGGLCONFIG'">
<div class="org-card__icon org-card__icon--amber">
<i class="fas fa-exchange-alt"></i>
</div>
<div class="org-card__body">
<h3 class="org-card__title">Clearing GL Configuration</h3>
<p class="org-card__desc">Map CTS, NACH and RTGS clearing roles to their GL accounts</p>
</div>
<i class="fas fa-chevron-right org-card__arrow"></i>
</a>
</div>
<!-- Currency Configuration -->
<div #addEditCurrency>
<a class="org-card" [routerLink]="['currencies']" *mifosxHasPermission="'READ_CURRENCY'">
@@ -888,4 +888,51 @@ export class OrganizationService {
deleteLoanOriginator(originatorId: number): Observable<any> {
return this.http.delete(`/loan-originators/${originatorId}`);
}
// ── Institution Profile ──
/** @returns {Observable<any>} The bank's own profile (name, legal name, code, contact...). */
getInstitutionProfile(): Observable<any> {
return this.http.get('/institutionprofile');
}
/** @returns {Observable<any>} */
updateInstitutionProfile(payload: any): Observable<any> {
return this.http.put('/institutionprofile', payload);
}
// ── Branch Profile ──
/** @returns {Observable<any[]>} Every office joined with its branch profile fields, if any. */
getBranchProfiles(): Observable<any> {
return this.http.get('/branchprofile');
}
/** @returns {Observable<any>} One office's branch profile, joined with its office fields. */
getBranchProfile(officeId: number): Observable<any> {
const params = new HttpParams().set('officeId', String(officeId));
return this.http.get('/branchprofile', { params });
}
/** @returns {Observable<any>} */
updateBranchProfile(officeId: number, payload: any): Observable<any> {
return this.http.put(`/branchprofile/${officeId}`, payload);
}
// ── Clearing GL Configuration ──
/** @returns {Observable<any[]>} Every clearing role (CTS/NACH/RTGS...) and its mapped GL account, if any. */
getClearingGlConfig(): Observable<any> {
return this.http.get('/clearingglconfig');
}
/** @returns {Observable<any>} Selectable GL accounts for mapping a clearing role. */
getClearingGlConfigTemplate(): Observable<any> {
return this.http.get('/clearingglconfig/template');
}
/** @returns {Observable<any>} */
updateClearingGlConfig(configKey: string, glAccountId: number | null): Observable<any> {
return this.http.put(`/clearingglconfig/${configKey}`, { glAccountId });
}
}