DDS: agent collection entry screen, per-agent GL account, maturity auto close

The Daily Transaction Entry screen no longer composes a Party Tran. An agent records their own round
elsewhere; this screen reads back what they wrote down, shows what it adds up to, and settles the
day once a clerk has counted the cash against it. Nothing is typed here and nothing is edited - what
settles is what the entries say, to the rupee.

Agent Master now asks which ledger account the agent's collections pass through, one agent to an
account, and offers only the accounts that can carry them. The DDS scheme product is kept out of the
agent's own savings list for the same reason the server refuses it: debits are blocked on those
accounts, so commission paid into one would go in and never come out.

The DDS product carries a Maturity Auto Close setting, asked as Yes/No to match the Auto Interest
Post question directly above it, and Batch Processing offers the run that applies it.

Also: the report viewer stops printing two money columns ungrouped and stops totalling a column that
holds a word, and an agent's birth date cannot be picked later than eighteen years ago.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tarunmalviya
2026-09-23 11:54:49 +05:30
co-authored by Claude Opus 5
parent 57a61b3057
commit e2eff9914f
114 changed files with 10259 additions and 15955 deletions
+5 -2
View File
@@ -14,6 +14,7 @@ import { CommonModule } from '@angular/common';
import { HasPermissionDirective } from './has-permission/has-permission.directive';
import { FormatAmountDirective } from './format-amount.directive';
import { ValidateOnFocusDirective } from './validate-on-focus.directive';
import { RestrictInputDirective } from './restrict-input.directive';
/**
* Directives Module
@@ -25,12 +26,14 @@ import { ValidateOnFocusDirective } from './validate-on-focus.directive';
CommonModule,
HasPermissionDirective,
FormatAmountDirective,
ValidateOnFocusDirective
ValidateOnFocusDirective,
RestrictInputDirective
],
exports: [
HasPermissionDirective,
FormatAmountDirective,
ValidateOnFocusDirective
ValidateOnFocusDirective,
RestrictInputDirective
]
})
export class DirectivesModule {}
@@ -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/.
*/
import { Directive, HostListener, Input, ElementRef, inject } from '@angular/core';
import { NgControl } from '@angular/forms';
/** The kinds of field this knows how to hold to a shape. */
export type RestrictKind = 'digits' | 'phone' | 'pan' | 'aadhaar';
interface Rule {
/** Characters this kind will accept at all. */
allowed: RegExp;
/** How many of them the field can hold, so it cannot be overfilled. */
maxLength: number;
/** Whether what is typed is forced to upper case as it is entered. */
upper?: boolean;
}
const RULES: Record<RestrictKind, Rule> = {
digits: { allowed: /[0-9]/, maxLength: 20 },
phone: { allowed: /[0-9+()\-\s]/, maxLength: 20 },
// A PAN is five letters, four digits, one letter - and is always written in capitals.
pan: { allowed: /[A-Za-z0-9]/, maxLength: 10, upper: true },
// Twelve digits, or the masked form CKYC returns, which is Xs and the last four digits.
aadhaar: { allowed: /[0-9Xx]/, maxLength: 12, upper: true }
};
/**
* Stops a field being filled with something it could never hold.
*
* A mobile number typed as letters, or a PAN of fifteen characters, is not a mistake worth
* discovering on save - the customer is at the counter now, and the clerk is looking at the field
* they just typed. Refusing the keystroke says so at the moment it happens, with nothing to read
* and nothing to undo.
*
* Pasting is handled too, and is where this earns its place: a number copied from a spreadsheet
* arrives with spaces, dashes or a +91 in front, and pasting is the one way into a field that
* bypasses every keystroke rule. What can be salvaged is kept, the rest dropped.
*
* This only refuses characters the field cannot contain. Whether what is left is a valid number is
* a separate question, answered by the form's own validators and again by the server - a screen
* restriction is a convenience, not a guarantee.
*/
@Directive({
selector: '[mifosxRestrictInput]',
standalone: true
})
export class RestrictInputDirective {
private el = inject(ElementRef<HTMLInputElement>);
private control = inject(NgControl, { optional: true });
@Input('mifosxRestrictInput') kind: RestrictKind = 'digits';
private get rule(): Rule {
return RULES[this.kind] ?? RULES.digits;
}
/**
* Keys that move around or edit rather than enter a character, plus the usual copy/paste and
* select-all chords. Blocking these would leave the clerk unable to correct their own typing.
*/
private isEditingKey(event: KeyboardEvent): boolean {
const navigation = [
'Backspace',
'Delete',
'Tab',
'Escape',
'Enter',
'Home',
'End',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown'
];
return navigation.includes(event.key) || event.ctrlKey || event.metaKey;
}
@HostListener('keydown', ['$event'])
onKeydown(event: KeyboardEvent): void {
if (this.isEditingKey(event) || event.key.length > 1) {
return;
}
const input = this.el.nativeElement as HTMLInputElement;
const hasSelection = input.selectionStart !== input.selectionEnd;
if (!this.rule.allowed.test(event.key)) {
event.preventDefault();
return;
}
// Replacing a selection does not make the field any longer, so only a plain insert can overfill.
if (!hasSelection && input.value.length >= this.rule.maxLength) {
event.preventDefault();
}
}
@HostListener('paste', ['$event'])
onPaste(event: ClipboardEvent): void {
event.preventDefault();
const pasted = event.clipboardData?.getData('text') ?? '';
const kept = Array.from(pasted)
.filter((ch) => this.rule.allowed.test(ch))
.join('')
.slice(0, this.rule.maxLength);
this.write(this.rule.upper ? kept.toUpperCase() : kept);
}
/** Upper case is applied as the field is left, so the caret does not jump while typing. */
@HostListener('blur')
onBlur(): void {
if (!this.rule.upper) {
return;
}
const current = String(this.el.nativeElement.value ?? '');
if (current !== current.toUpperCase()) {
this.write(current.toUpperCase());
}
}
/**
* Writes through the form control where there is one, so the value the form holds and the value
* on screen stay the same thing - setting only the element would leave the form with the old one.
*/
private write(value: string): void {
this.el.nativeElement.value = value;
if (this.control?.control) {
this.control.control.setValue(value);
} else {
this.el.nativeElement.dispatchEvent(new Event('input'));
}
}
}