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,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')}`;
}
}