feat: letter-mgmt module + digital UPI/IMPS/ATM-POS reports + header redesign

letter-mgmt (new module):
- Notice page: overdue loan list with filters, KPI strip, bulk actions,
  call disposition logging (Promise-to-Pay, Not Reachable, etc.), Speed
  Post dispatch dialog with consignment + post-office tracking,
  PDF preview + download, call-contact dialog with primary / alt /
  family / nominee numbers and per-contact disposition history.
- Certificates: Loan Interest, Share, Balance, Form 60 (no-PAN flow),
  Form 15G/H (auto age-based), Hypothecation, NOC - each with shared
  preview dialog (six tone variants) and jsPDF generator using a
  common letterhead / summary-grid / signature-seal helper.

digital:
- IMPS module: report (P2A/P2P/Aadhaar tabs) + entry form with
  MMID/mobile lookup, NPCI switch resolve-to-A/C+IFSC BANL flow,
  sender autocomplete, hide sender field for P2P/Aadhaar, charges +
  18% GST + waive checkbox, per-sender today-only history dialog.
- UPI report with app-mix tab and full transaction filtering.
- ATM/POS/ECOM hub with three ticket cards routing to today/status/cycle.
- NEFT/RTGS entry: sender-account autocomplete, charges + GST + waive,
  field reorder (sender before amount), NEFT cap / RTGS floor
  validation, History button opening per-sender today's transactions.
- NEFT/RTGS report: horizontal-bar trend leaderboard replacing tall bars.

shell + theme:
- Header redesigned with IBM Plex Sans 12/500 typography, 11-column grid
  per row, top-accent hover indicator, bordered cells via grid-gap.
- Live KPI ticker below menu (CASH, TXN TODAY, CLEARING IN/OUT, NPA,
  CASA, CD RATIO, RBI CIRC, REPO) with marquee scroll, pause-on-hover,
  brand-blue gradient, amber bottom strip, pulsing red LIVE chip.
- Sidebar 'KYC Management' -> 'KYC & ReKYC Management'.
- Letter Mgmt / certificate menu items wired with green icon hints for
  items that have a working page.

access-control:
- MFA card layout compacted; method header inlines back button + title.

icons:
- Register faTruck/TruckFast, faEllipsisV/H, faCommentDots.
- Add IBM Plex Sans Google Fonts import to index.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-19 10:09:06 +05:30
co-authored by Claude Opus 4.7
parent 4322f37c6d
commit 325228c514
138 changed files with 27473 additions and 3997 deletions
@@ -6,121 +6,150 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-card>
<form [formGroup]="clientChargeForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field>
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select required formControlName="chargeId">
@for (clientCharge of clientChargeOptions; track clientCharge) {
<mat-option [value]="clientCharge.id">
{{ clientCharge.name + ' (' + clientCharge.currency.name + ')' }}
</mat-option>
}
</mat-select>
@if (clientChargeForm.controls.chargeId.hasError('required')) {
<div class="ca-root">
<div class="ca-hero">
<div class="ca-hero-icon"><fa-icon icon="receipt"></fa-icon></div>
<div class="ca-hero-text">
<div class="ca-hero-eyebrow">Client Action</div>
<h2 class="ca-hero-title">{{ 'labels.buttons.Add Charge' | translate }}</h2>
<p class="ca-hero-sub">
Attach a one-time fee or a recurring charge to this client. The charge can be linked to a specific account or
held at the client level for ad-hoc collection.
</p>
</div>
</div>
<form [formGroup]="clientChargeForm" (ngSubmit)="submit()" class="ca-card">
<div class="ca-card-body">
<div class="ca-form-row">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
<mat-select required formControlName="chargeId">
@for (clientCharge of clientChargeOptions; track clientCharge) {
<mat-option [value]="clientCharge.id">{{
clientCharge.name + ' (' + clientCharge.currency.name + ')'
}}</mat-option>
}
</mat-select>
@if (clientChargeForm.controls.chargeId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</div>
@if (chargeDetails) {
<div class="ca-form-row">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Amount' | translate }}</mat-label>
<input type="number" required matInput formControlName="amount" />
@if (clientChargeForm.controls.amount.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Charge' | translate }} {{ 'labels.commons.is' | translate }}
{{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
@if (chargeDetails) {
<div class="layout-column">
<mat-form-field>
<mat-label>{{ 'labels.inputs.Amount' | translate }}</mat-label>
<input type="number" required matInput formControlName="amount" />
@if (clientChargeForm.controls.amount.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Amount' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Charge Calculation' | translate }}</mat-label>
<mat-select formControlName="chargeCalculationType">
@for (chargeCalculation of chargeDetails.chargeCalculationTypeOptions; track chargeCalculation) {
<mat-option [value]="chargeCalculation.id">
{{ chargeCalculation.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'labels.inputs.Charge time type' | translate }}</mat-label>
<mat-select formControlName="chargeTimeType">
@for (chargeTime of chargeDetails.chargeTimeTypeOptions; track chargeTime) {
<mat-option [value]="chargeTime.id">
{{ chargeTime.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (clientChargeForm.contains('dueDate')) {
<mat-form-field (click)="dueDatePicker.open()">
<mat-label>{{ 'labels.inputs.Due for collection on' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="dueDatePicker"
required
formControlName="dueDate"
/>
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dueDatePicker></mat-datepicker>
@if (clientChargeForm.controls.dueDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due for collection on' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
@if (clientChargeForm.contains('feeOnMonthDay')) {
<mat-form-field (click)="feeOnMonthDayPicker.open()">
<mat-label>{{ 'labels.inputs.Due On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="feeOnMonthDayPicker"
required
formControlName="feeOnMonthDay"
/>
<mat-datepicker-toggle matSuffix [for]="feeOnMonthDayPicker"></mat-datepicker-toggle>
<mat-datepicker #feeOnMonthDayPicker></mat-datepicker>
@if (clientChargeForm.controls.feeOnMonthDay.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
@if (clientChargeForm.contains('feeInterval')) {
<mat-form-field>
<mat-label>{{ 'labels.inputs.Repeats Every' | translate }}</mat-label>
<input matInput formControlName="feeInterval" />
</mat-form-field>
}
</div>
}
</div>
<mat-card-actions class="layout-row layout-xs-column layout-align-center gap-5px">
<button type="button" mat-raised-button [routerLink]="['../../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!clientChargeForm.valid">
{{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</mat-card-content>
</form>
</mat-card>
<div class="ca-form-row">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Charge Calculation' | translate }}</mat-label>
<mat-select formControlName="chargeCalculationType">
@for (chargeCalculation of chargeDetails.chargeCalculationTypeOptions; track chargeCalculation) {
<mat-option [value]="chargeCalculation.id">{{ chargeCalculation.value }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="ca-form-row">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Charge time type' | translate }}</mat-label>
<mat-select formControlName="chargeTimeType">
@for (chargeTime of chargeDetails.chargeTimeTypeOptions; track chargeTime) {
<mat-option [value]="chargeTime.id">{{ chargeTime.value }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (clientChargeForm.contains('dueDate')) {
<div class="ca-form-row">
<mat-form-field appearance="outline" (click)="dueDatePicker.open()">
<mat-label>{{ 'labels.inputs.Due for collection on' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="dueDatePicker"
required
formControlName="dueDate"
/>
<mat-datepicker-toggle matSuffix [for]="dueDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dueDatePicker></mat-datepicker>
@if (clientChargeForm.controls.dueDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due for collection on' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</div>
}
@if (clientChargeForm.contains('feeOnMonthDay')) {
<div class="ca-form-row">
<mat-form-field appearance="outline" (click)="feeOnMonthDayPicker.open()">
<mat-label>{{ 'labels.inputs.Due On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="feeOnMonthDayPicker"
required
formControlName="feeOnMonthDay"
/>
<mat-datepicker-toggle matSuffix [for]="feeOnMonthDayPicker"></mat-datepicker-toggle>
<mat-datepicker #feeOnMonthDayPicker></mat-datepicker>
@if (clientChargeForm.controls.feeOnMonthDay.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Due Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
</div>
}
@if (clientChargeForm.contains('feeInterval')) {
<div class="ca-form-row">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Repeats Every' | translate }}</mat-label>
<input matInput formControlName="feeInterval" />
</mat-form-field>
</div>
}
<div class="ca-notice">
<fa-icon icon="info-circle"></fa-icon>
<span>
The calculation type and timing determine when this charge is auto-applied (e.g. monthly fee, annual
membership, ad-hoc). Verify the amount and due date before submitting.
</span>
</div>
}
</div>
<div class="ca-actions">
<button type="button" class="ca-btn ca-btn--cancel" [routerLink]="['../../']">
<fa-icon icon="times"></fa-icon><span>{{ 'labels.buttons.Cancel' | translate }}</span>
</button>
<button type="submit" class="ca-btn ca-btn--confirm" [disabled]="!clientChargeForm.valid">
<fa-icon icon="check"></fa-icon><span>{{ 'labels.buttons.Submit' | translate }}</span>
</button>
</div>
</form>
</div>
@@ -6,6 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
width: 37rem;
@use 'assets/styles/client-action' as ca;
@include ca.client-action-styles;
/* Add Charge — amber accent (financial / fee) */
:host {
--ca-accent: #b45309;
--ca-accent-l: #fef3c7;
}
@@ -21,6 +21,7 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { ClientsService } from 'app/clients/clients.service';
import { Dates } from 'app/core/utils/dates';
import { SettingsService } from 'app/settings/settings.service';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -31,7 +32,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
templateUrl: './add-client-charge.component.html',
styleUrls: ['./add-client-charge.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
...STANDALONE_SHARED_IMPORTS,
FaIconComponent
]
})
export class AddClientChargeComponent implements OnInit {