- Correct Investment portfolio allocation and Loan Origination product-mix targets against RBI's UCB SLR/Non-SLR framework and PSL circulars, with a Target Basis column distinguishing RBI-mandated vs board-policy rows (Excel export included). - Expand Investment holdings dataset with more near-term maturities so the Due-Date Diary reflects a realistic spread across FD/Govt Security/Non-SLR/Bond. - Add From/To period-based Interest Calculation & Post (with GL selection for both posting and receiving) across the Investment Interest module, FD Entry and Bond Entry screens; fix a bug where a posted period kept re-appearing as due since accrual wasn't tracked per-holding. - Add a reusable DD-MM-YYYY date-picker component and roll it out across all Investment date fields. - NPA Summary Report: drop SMA-0/1/2 from the stage-wise table (kept in Day-wise Overdue only). - NPA Recovery Report: add Account Type / NPA Category / Target-vs-Actual columns, a shared Account Type filter driving both the account picker and the register, and a searchable (fixed-position, not clipped by card overflow) account select for long lists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
214 lines
5.6 KiB
TypeScript
214 lines
5.6 KiB
TypeScript
/**
|
|
* 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, ElementRef, ViewChild, forwardRef } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
|
|
/**
|
|
* Drop-in replacement for `<input type="text" placeholder="DD-MM-YYYY">` used throughout the
|
|
* Investment module. Keeps the DD-MM-YYYY string every date field/helper in investment-data.ts
|
|
* already expects (parseDDMMYYYY, addPeriod, etc.), but adds a calendar button that opens the
|
|
* browser's native date picker and converts the picked value back to DD-MM-YYYY — so users can
|
|
* either type the date or pick it, without changing any consuming component's data model.
|
|
*
|
|
* Usage: <mifosx-date-ddmmyyyy [(ngModel)]="someField"></mifosx-date-ddmmyyyy>
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-date-ddmmyyyy',
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule
|
|
],
|
|
providers: [
|
|
{
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => DateDdmmyyyyPickerComponent),
|
|
multi: true
|
|
}
|
|
],
|
|
template: `
|
|
<div class="inv-date-field">
|
|
<input
|
|
type="text"
|
|
class="inv-date-field__text"
|
|
[ngModel]="value"
|
|
(ngModelChange)="onTextChange($event)"
|
|
(blur)="onTouched()"
|
|
placeholder="DD-MM-YYYY"
|
|
[disabled]="disabled"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="inv-date-field__btn"
|
|
tabindex="-1"
|
|
[disabled]="disabled"
|
|
(click)="openPicker()"
|
|
aria-label="Pick a date"
|
|
title="Pick a date"
|
|
>
|
|
📅
|
|
</button>
|
|
<input
|
|
#native
|
|
type="date"
|
|
class="inv-date-field__native"
|
|
tabindex="-1"
|
|
[ngModel]="isoValue"
|
|
(ngModelChange)="onPickerChange($event)"
|
|
/>
|
|
</div>
|
|
`,
|
|
styles: [
|
|
`
|
|
/* Angular view encapsulation stops the host page's .inv-form-field styles from reaching
|
|
* inside this component's own template, so the look of the surrounding form fields
|
|
* (height, border, radius, font) is deliberately re-declared here instead of inherited. */
|
|
:host {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
.inv-date-field {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
}
|
|
.inv-date-field__text {
|
|
flex: 1;
|
|
min-width: 0;
|
|
width: 100%;
|
|
height: 36px;
|
|
padding: 0 10px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
color: #0d1b2a;
|
|
font-family: inherit;
|
|
background: #fff;
|
|
box-sizing: border-box;
|
|
}
|
|
.inv-date-field__btn {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 36px;
|
|
width: 36px;
|
|
padding: 0;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
}
|
|
.inv-date-field__btn:hover:not(:disabled) {
|
|
background: #f2f4f7;
|
|
}
|
|
.inv-date-field__btn:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.5;
|
|
}
|
|
.inv-date-field__native {
|
|
position: absolute;
|
|
width: 0;
|
|
height: 0;
|
|
padding: 0;
|
|
border: none;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
`
|
|
]
|
|
})
|
|
export class DateDdmmyyyyPickerComponent implements ControlValueAccessor {
|
|
@ViewChild('native') private nativeInput?: ElementRef<HTMLInputElement>;
|
|
|
|
value = '';
|
|
disabled = false;
|
|
|
|
private onChangeFn: (v: string) => void = () => {};
|
|
onTouched: () => void = () => {};
|
|
|
|
/** DD-MM-YYYY -> yyyy-MM-dd, the format <input type="date"> requires. */
|
|
get isoValue(): string {
|
|
const parts = (this.value || '').split('-');
|
|
if (parts.length !== 3) {
|
|
return '';
|
|
}
|
|
const [
|
|
dd,
|
|
mm,
|
|
yyyy
|
|
] = parts;
|
|
if (!/^\d{2}$/.test(dd) || !/^\d{2}$/.test(mm) || !/^\d{4}$/.test(yyyy)) {
|
|
return '';
|
|
}
|
|
return `${yyyy}-${mm}-${dd}`;
|
|
}
|
|
|
|
writeValue(v: string): void {
|
|
this.value = v ?? '';
|
|
}
|
|
|
|
registerOnChange(fn: (v: string) => void): void {
|
|
this.onChangeFn = fn;
|
|
}
|
|
|
|
registerOnTouched(fn: () => void): void {
|
|
this.onTouched = fn;
|
|
}
|
|
|
|
setDisabledState(isDisabled: boolean): void {
|
|
this.disabled = isDisabled;
|
|
}
|
|
|
|
onTextChange(v: string): void {
|
|
this.value = v;
|
|
this.onChangeFn(v);
|
|
}
|
|
|
|
/** yyyy-MM-dd (from the native picker) -> DD-MM-YYYY, the format the rest of the app expects. */
|
|
onPickerChange(iso: string): void {
|
|
const parts = (iso || '').split('-');
|
|
if (parts.length !== 3) {
|
|
return;
|
|
}
|
|
const [
|
|
yyyy,
|
|
mm,
|
|
dd
|
|
] = parts;
|
|
const ddmmyyyy = `${dd}-${mm}-${yyyy}`;
|
|
this.value = ddmmyyyy;
|
|
this.onChangeFn(ddmmyyyy);
|
|
this.onTouched();
|
|
}
|
|
|
|
openPicker(): void {
|
|
const el = this.nativeInput?.nativeElement;
|
|
if (!el || this.disabled) {
|
|
return;
|
|
}
|
|
const showPicker = (el as HTMLInputElement & { showPicker?: () => void }).showPicker;
|
|
if (typeof showPicker === 'function') {
|
|
try {
|
|
showPicker.call(el);
|
|
return;
|
|
} catch {
|
|
// Some browsers throw if not called from a "user gesture" context they recognise —
|
|
// fall back to a plain click(), which still opens the native picker.
|
|
}
|
|
}
|
|
el.click();
|
|
}
|
|
}
|