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