form 91/98 report, min max value update in AML rule and whihc rule apply day, week, month and some more updates
This commit is contained in:
@@ -21,6 +21,22 @@
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content class="aml-card-content">
|
||||
<!-- Separate the rules by the window they are measured over -->
|
||||
<div class="period-filters">
|
||||
<span class="period-filters-label">Applies over</span>
|
||||
<button
|
||||
*ngFor="let p of periodFilters"
|
||||
type="button"
|
||||
class="period-chip"
|
||||
[class.active]="activePeriod === p.key"
|
||||
[disabled]="periodCount(p.key) === 0"
|
||||
(click)="setPeriod(p.key)"
|
||||
>
|
||||
{{ p.label }}
|
||||
<span class="period-chip-n">{{ periodCount(p.key) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="rules" class="aml-table mat-elevation-z1">
|
||||
<!-- Order Position Column -->
|
||||
<ng-container matColumnDef="orderPosition">
|
||||
@@ -45,10 +61,37 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Income / Occupation Limit Column -->
|
||||
<ng-container matColumnDef="incomeLimit">
|
||||
<th mat-header-cell *matHeaderCellDef>Income / Occupation Limit</th>
|
||||
<td mat-cell *matCellDef="let element" class="income-limit-cell">{{ getIncomeLimitLabel(element) }}</td>
|
||||
<!-- What the min/max figures are measured against -->
|
||||
<ng-container matColumnDef="limitBasis">
|
||||
<th mat-header-cell *matHeaderCellDef>Limit Basis</th>
|
||||
<td mat-cell *matCellDef="let element" class="basis-cell">
|
||||
{{ getLimitBasis(element) }}
|
||||
<span *ngIf="getNonNumericCriterion(element)" class="basis-sub">
|
||||
{{ getNonNumericCriterion(element) }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Minimum Limit Column -->
|
||||
<ng-container matColumnDef="minLimit">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-right">Minimum Limit</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-right amount-cell">{{ getMinLimit(element) }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Maximum Limit Column -->
|
||||
<ng-container matColumnDef="maxLimit">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-right">Maximum Limit</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-right amount-cell">
|
||||
<span [class.no-ceiling]="getMaxLimit(element) === 'No upper limit'">{{ getMaxLimit(element) }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Observation window the rule is measured over -->
|
||||
<ng-container matColumnDef="appliesOver">
|
||||
<th mat-header-cell *matHeaderCellDef class="col-center">Applies Over</th>
|
||||
<td mat-cell *matCellDef="let element" class="col-center">
|
||||
<span [ngClass]="periodClass(element)">{{ periodLabel(element) }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Reason Column -->
|
||||
|
||||
@@ -114,13 +114,141 @@
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.income-limit-cell {
|
||||
.col-right {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* What the min/max amounts are measured against. */
|
||||
.basis-cell {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: normal;
|
||||
max-width: 200px;
|
||||
line-height: 1.35;
|
||||
max-width: 150px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.basis-sub {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.amount-cell {
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 12.5px;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* An open-ended threshold rule has a floor but no ceiling. */
|
||||
.no-ceiling {
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* ── Observation-window badges ─────────────────────────────────────────── */
|
||||
.period-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
&.period-txn {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
&.period-day {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
&.period-week {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
&.period-month {
|
||||
background: #ede9fe;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
&.period-other {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chips that separate the rule list by window. */
|
||||
.period-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 0 14px;
|
||||
}
|
||||
|
||||
.period-filters-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.period-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #1a237e;
|
||||
color: #1a237e;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1a237e;
|
||||
border-color: #1a237e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.period-chip-n {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
|
||||
.period-chip.active & {
|
||||
background: rgb(255 255 255 / 22%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Badges for Risk Category */
|
||||
|
||||
@@ -42,24 +42,185 @@ export class AmlRulesComponent implements OnInit {
|
||||
'orderPosition',
|
||||
'ruleName',
|
||||
'riskCategory',
|
||||
'incomeLimit',
|
||||
'limitBasis',
|
||||
'minLimit',
|
||||
'maxLimit',
|
||||
'appliesOver',
|
||||
'reason',
|
||||
'amlCheckStatus',
|
||||
'bannedEntityMatch'
|
||||
];
|
||||
|
||||
/** Every rule as returned by the API, before the period filter is applied. */
|
||||
private allRules: any[] = [];
|
||||
|
||||
/** Period buckets the rules can be separated into. */
|
||||
readonly periodFilters: { key: string; label: string }[] = [
|
||||
{ key: '', label: 'All rules' },
|
||||
{ key: 'TXN', label: 'Per transaction' },
|
||||
{ key: 'DAY', label: 'Daily' },
|
||||
{ key: 'WEEK', label: 'Weekly' },
|
||||
{ key: 'MONTH', label: 'Monthly' },
|
||||
{ key: 'OTHER', label: 'Other window' }
|
||||
];
|
||||
activePeriod = '';
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientsService.getClientRiskRules().subscribe({
|
||||
next: (data) => {
|
||||
this.rules.data = data || [];
|
||||
this.allRules = data || [];
|
||||
this.applyPeriodFilter();
|
||||
this.rules.paginator = this.paginator;
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
setPeriod(key: string): void {
|
||||
this.activePeriod = key;
|
||||
this.applyPeriodFilter();
|
||||
}
|
||||
|
||||
private applyPeriodFilter(): void {
|
||||
this.rules.data = this.activePeriod
|
||||
? this.allRules.filter((r) => this.periodKey(r) === this.activePeriod)
|
||||
: this.allRules;
|
||||
if (this.rules.paginator) {
|
||||
this.rules.paginator.firstPage();
|
||||
}
|
||||
}
|
||||
|
||||
/** How many rules fall in each period bucket — shown on the filter chips. */
|
||||
periodCount(key: string): number {
|
||||
return key ? this.allRules.filter((r) => this.periodKey(r) === key).length : this.allRules.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buckets a rule by the observation window it is measured over.
|
||||
* A rule with no window is evaluated on the single transaction itself, so it is
|
||||
* in force every day rather than accumulating over a period.
|
||||
*/
|
||||
periodKey(rule: any): string {
|
||||
const days = rule.txnWindowDays;
|
||||
if (days == null || days === 0) {
|
||||
return 'TXN';
|
||||
}
|
||||
if (days === 1) {
|
||||
return 'DAY';
|
||||
}
|
||||
if (days === 7) {
|
||||
return 'WEEK';
|
||||
}
|
||||
if (days >= 28 && days <= 31) {
|
||||
return 'MONTH';
|
||||
}
|
||||
return 'OTHER';
|
||||
}
|
||||
|
||||
periodLabel(rule: any): string {
|
||||
const days = rule.txnWindowDays;
|
||||
switch (this.periodKey(rule)) {
|
||||
case 'TXN':
|
||||
return 'Per transaction';
|
||||
case 'DAY':
|
||||
return 'Daily';
|
||||
case 'WEEK':
|
||||
return 'Weekly';
|
||||
case 'MONTH':
|
||||
return 'Monthly';
|
||||
default:
|
||||
return `${days} days`;
|
||||
}
|
||||
}
|
||||
|
||||
periodClass(rule: any): string {
|
||||
return 'period-badge period-' + this.periodKey(rule).toLowerCase();
|
||||
}
|
||||
|
||||
/** Sub-caption telling the reader what the min/max figures actually measure. */
|
||||
getLimitBasis(rule: any): string {
|
||||
if (rule.minMonthlyIncome > 0 || rule.maxMonthlyIncome > 0) {
|
||||
return 'Monthly Income';
|
||||
}
|
||||
if (rule.minAnnualIncome > 0 || rule.maxAnnualIncome > 0) {
|
||||
return 'Annual Income';
|
||||
}
|
||||
if (rule.minAnnualTurnover > 0 || rule.maxAnnualTurnover > 0) {
|
||||
return 'Annual Turnover';
|
||||
}
|
||||
if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) {
|
||||
return 'Txn Amount (tiered)';
|
||||
}
|
||||
if (rule.txnThresholdAmount != null) {
|
||||
return 'Txn Amount';
|
||||
}
|
||||
if (rule.occupationName) {
|
||||
return 'Occupation';
|
||||
}
|
||||
if (rule.occupationCategoryName) {
|
||||
return 'Occupation Category';
|
||||
}
|
||||
return 'Behavioural pattern';
|
||||
}
|
||||
|
||||
private inr(n: number): string {
|
||||
return '₹' + Number(n).toLocaleString('en-IN');
|
||||
}
|
||||
|
||||
/** Lower bound of the rule, or an em dash where the criterion is not numeric. */
|
||||
getMinLimit(rule: any): string {
|
||||
if (rule.minMonthlyIncome > 0) {
|
||||
return this.inr(rule.minMonthlyIncome);
|
||||
}
|
||||
if (rule.minAnnualIncome > 0) {
|
||||
return this.inr(rule.minAnnualIncome);
|
||||
}
|
||||
if (rule.minAnnualTurnover > 0) {
|
||||
return this.inr(rule.minAnnualTurnover);
|
||||
}
|
||||
// Declarative band from m_aml_transaction_rule takes precedence over the raw threshold.
|
||||
if (rule.txnMinLimitAmount != null) {
|
||||
return this.inr(rule.txnMinLimitAmount);
|
||||
}
|
||||
if (rule.txnThresholdAmount != null) {
|
||||
return this.inr(rule.txnThresholdAmount);
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
/** Upper bound, or "No upper limit" where the rule is genuinely open-ended. */
|
||||
getMaxLimit(rule: any): string {
|
||||
if (rule.maxMonthlyIncome > 0) {
|
||||
return this.inr(rule.maxMonthlyIncome);
|
||||
}
|
||||
if (rule.maxAnnualIncome > 0) {
|
||||
return this.inr(rule.maxAnnualIncome);
|
||||
}
|
||||
if (rule.maxAnnualTurnover > 0) {
|
||||
return this.inr(rule.maxAnnualTurnover);
|
||||
}
|
||||
if (rule.txnMaxLimitAmount != null) {
|
||||
return this.inr(rule.txnMaxLimitAmount);
|
||||
}
|
||||
// A floor with no configured ceiling means the rule has no upper bound.
|
||||
if (
|
||||
rule.minMonthlyIncome > 0 ||
|
||||
rule.minAnnualIncome > 0 ||
|
||||
rule.minAnnualTurnover > 0 ||
|
||||
rule.txnThresholdAmount != null
|
||||
) {
|
||||
return 'No upper limit';
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
/** Occupation-driven rules carry no amount; show the criterion instead. */
|
||||
getNonNumericCriterion(rule: any): string {
|
||||
return rule.occupationName || rule.occupationCategoryName || '';
|
||||
}
|
||||
|
||||
getRiskCategoryLabel(category: number): string {
|
||||
switch (category) {
|
||||
case 1:
|
||||
@@ -85,39 +246,4 @@ export class AmlRulesComponent implements OnInit {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Formats whichever income/turnover/occupation criterion drives this rule's risk mark. */
|
||||
getIncomeLimitLabel(rule: any): string {
|
||||
const inr = (n: number) => '₹' + Number(n).toLocaleString('en-IN');
|
||||
if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0) {
|
||||
return rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0
|
||||
? `Monthly Income ${inr(rule.minMonthlyIncome)} - ${inr(rule.maxMonthlyIncome)}`
|
||||
: `Monthly Income > ${inr(rule.minMonthlyIncome)}`;
|
||||
}
|
||||
if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0) {
|
||||
return rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0
|
||||
? `Annual Income ${inr(rule.minAnnualIncome)} - ${inr(rule.maxAnnualIncome)}`
|
||||
: `Annual Income > ${inr(rule.minAnnualIncome)}`;
|
||||
}
|
||||
if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0) {
|
||||
return rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0
|
||||
? `Annual Turnover ${inr(rule.minAnnualTurnover)} - ${inr(rule.maxAnnualTurnover)}`
|
||||
: `Annual Turnover > ${inr(rule.minAnnualTurnover)}`;
|
||||
}
|
||||
if (rule.occupationName) return `Occupation: ${rule.occupationName}`;
|
||||
if (rule.occupationCategoryName) return `Occupation Category: ${rule.occupationCategoryName}`;
|
||||
if (rule.txnThresholdAmountMediumRisk != null || rule.txnThresholdAmountHighRisk != null) {
|
||||
const parts: string[] = [];
|
||||
if (rule.txnThresholdAmountMediumRisk != null) parts.push(`${inr(rule.txnThresholdAmountMediumRisk)} (Medium Risk)`);
|
||||
if (rule.txnThresholdAmountHighRisk != null) parts.push(`${inr(rule.txnThresholdAmountHighRisk)} (High Risk)`);
|
||||
return `Txn Amount ≥ ${parts.join(' / ')}`;
|
||||
}
|
||||
if (rule.txnThresholdAmount != null) {
|
||||
const amt = inr(rule.txnThresholdAmount);
|
||||
if (rule.txnRuleType === 'THRESHOLD') return `Txn Amount ≥ ${amt}`;
|
||||
if (rule.txnWindowDays != null) return `${amt} within ${rule.txnWindowDays} days`;
|
||||
return `Txn Amount ≥ ${amt}`;
|
||||
}
|
||||
return 'Behavioral / Transaction Pattern';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<!--
|
||||
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="glm-root">
|
||||
<div class="glm-head">
|
||||
<div>
|
||||
<h3>Map General Ledger accounts</h3>
|
||||
<p>{{ data.returnCode }} · {{ data.lineLabel }}</p>
|
||||
</div>
|
||||
<button class="glm-btn" (click)="cancel()"><fa-icon icon="times"></fa-icon></button>
|
||||
</div>
|
||||
|
||||
<!-- Live comparison between the ledger and the figure on the return -->
|
||||
<div
|
||||
class="glm-compare"
|
||||
[class.glm-compare--match]="selected.length && matchesFiled"
|
||||
[class.glm-compare--diff]="selected.length && !matchesFiled && variance !== null"
|
||||
>
|
||||
<div class="glm-fig">
|
||||
<span class="glm-fig-l">On the return</span>
|
||||
<span class="glm-fig-v">{{ data.filedValue === null ? '—' : (data.filedValue | number: '1.2-2') }}</span>
|
||||
</div>
|
||||
<fa-icon icon="exchange-alt" class="glm-fig-ic"></fa-icon>
|
||||
<div class="glm-fig">
|
||||
<span class="glm-fig-l">From the ledger</span>
|
||||
<span class="glm-fig-v">{{ selected.length ? (derivedValue | number: '1.2-2') : '—' }}</span>
|
||||
</div>
|
||||
@if (selected.length && variance !== null) {
|
||||
<div class="glm-fig glm-fig--var">
|
||||
<span class="glm-fig-l">Variance</span>
|
||||
<span class="glm-fig-v">{{ variance | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="glm-compare-note">
|
||||
@if (!selected.length) {
|
||||
Select accounts below to derive this line from the ledger.
|
||||
} @else if (matchesFiled) {
|
||||
Ledger agrees with the filed figure.
|
||||
} @else {
|
||||
Ledger differs from the filed figure — the mapped value will be shown on the return.
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Currently pinned accounts -->
|
||||
<div class="glm-selected">
|
||||
<div class="glm-selected-h">
|
||||
<span>Mapped accounts ({{ selected.length }})</span>
|
||||
@if (selected.length) {
|
||||
<button class="glm-link" (click)="clearAll()">Remove all</button>
|
||||
}
|
||||
</div>
|
||||
@if (selected.length) {
|
||||
<div class="glm-chips">
|
||||
@for (code of selected; track code) {
|
||||
<span class="glm-chip">
|
||||
<span class="mono">{{ code }}</span> {{ accountName(code) }}
|
||||
<button class="glm-chip-x" (click)="removeSelected(code)" [attr.aria-label]="'Remove ' + code">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="glm-empty">No accounts mapped — this line still uses the figure filed on the return.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Chart of accounts picker -->
|
||||
<div class="glm-filters">
|
||||
<input class="glm-inp" type="text" placeholder="Search by code, name or group…" [(ngModel)]="search" />
|
||||
<select class="glm-inp glm-inp--sm" [(ngModel)]="typeFilter">
|
||||
<option value="">All types</option>
|
||||
@for (t of types; track t) {
|
||||
<option [value]="t">{{ t }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="glm-list">
|
||||
<table class="glm-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ta-c">Map</th>
|
||||
<th>GL Code</th>
|
||||
<th>Account</th>
|
||||
<th>Type</th>
|
||||
<th>Group</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (a of filtered; track a.code) {
|
||||
<tr [class.on]="isSelected(a.code)" (click)="toggle(a.code)">
|
||||
<td class="ta-c">
|
||||
<span class="glm-check" [class.on]="isSelected(a.code)">
|
||||
@if (isSelected(a.code)) {
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
<td class="mono">{{ a.code }}</td>
|
||||
<td>{{ a.name }}</td>
|
||||
<td>
|
||||
<span class="glm-type" [class]="'t-' + a.type.toLowerCase()">{{ a.type }}</span>
|
||||
</td>
|
||||
<td class="muted">{{ a.group }}</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5" class="glm-empty-row">No accounts match that search.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="glm-actions">
|
||||
<button class="glm-btn" (click)="cancel()">Cancel</button>
|
||||
<button class="glm-btn glm-btn-primary" (click)="save()">Save mapping ({{ selected.length }})</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
--m-navy: #002f5f;
|
||||
--m-green: #15803d;
|
||||
--m-green-l: #dcfce7;
|
||||
--m-amber: #b45309;
|
||||
--m-amber-l: #fef3c7;
|
||||
--m-red: #c0392b;
|
||||
--m-red-l: #fdecea;
|
||||
--m-g50: #f8fafc;
|
||||
--m-g100: #f1f5f9;
|
||||
--m-g200: #e2e8f0;
|
||||
--m-g300: #cbd5e1;
|
||||
--m-g500: #64748b;
|
||||
--m-g700: #334155;
|
||||
--m-g900: #0f172a;
|
||||
|
||||
display: block;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
color: var(--m-g900);
|
||||
}
|
||||
|
||||
.glm-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px 20px;
|
||||
max-height: 88vh;
|
||||
}
|
||||
|
||||
.glm-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--m-g500);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ledger vs filed figure */
|
||||
.glm-compare {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
background: var(--m-g100);
|
||||
border: 1px solid var(--m-g200);
|
||||
|
||||
&--match {
|
||||
background: var(--m-green-l);
|
||||
border-color: #86efac;
|
||||
}
|
||||
|
||||
&--diff {
|
||||
background: var(--m-amber-l);
|
||||
border-color: #fcd34d;
|
||||
}
|
||||
}
|
||||
|
||||
.glm-fig {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--var .glm-fig-v {
|
||||
color: var(--m-amber);
|
||||
}
|
||||
}
|
||||
|
||||
.glm-fig-l {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--m-g500);
|
||||
}
|
||||
|
||||
.glm-fig-v {
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.glm-fig-ic {
|
||||
color: var(--m-g500);
|
||||
}
|
||||
|
||||
.glm-compare-note {
|
||||
flex: 1 1 220px;
|
||||
font-size: 12px;
|
||||
color: var(--m-g700);
|
||||
}
|
||||
|
||||
/* Pinned accounts */
|
||||
.glm-selected-h {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--m-g500);
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.glm-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.glm-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: #e0f2fe;
|
||||
color: #075985;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.glm-chip-x {
|
||||
border: none;
|
||||
background: rgb(7 89 133 / 12%);
|
||||
color: #075985;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
|
||||
&:hover {
|
||||
background: var(--m-red);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.glm-empty {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--m-g500);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Picker */
|
||||
.glm-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.glm-inp {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--m-g200);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
|
||||
&--sm {
|
||||
flex: 0 0 160px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--m-navy);
|
||||
}
|
||||
}
|
||||
|
||||
.glm-list {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--m-g200);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.glm-tbl {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--m-g500);
|
||||
background: var(--m-g50);
|
||||
border-bottom: 1px solid var(--m-g200);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 7px 12px;
|
||||
border-bottom: 1px solid var(--m-g100);
|
||||
font-size: 13px;
|
||||
color: var(--m-g700);
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover td {
|
||||
background: var(--m-g50);
|
||||
}
|
||||
|
||||
&.on td {
|
||||
background: #eff6ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.glm-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--m-g300);
|
||||
border-radius: 5px;
|
||||
color: transparent;
|
||||
font-size: 11px;
|
||||
|
||||
&.on {
|
||||
background: var(--m-green);
|
||||
border-color: var(--m-green);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.glm-type {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: var(--m-g100);
|
||||
color: var(--m-g700);
|
||||
|
||||
&.t-asset {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
&.t-liability {
|
||||
background: #e0f4f0;
|
||||
color: #0e7e6b;
|
||||
}
|
||||
|
||||
&.t-income {
|
||||
background: var(--m-green-l);
|
||||
color: var(--m-green);
|
||||
}
|
||||
|
||||
&.t-expense {
|
||||
background: var(--m-amber-l);
|
||||
color: var(--m-amber);
|
||||
}
|
||||
|
||||
&.t-contingent {
|
||||
background: var(--m-red-l);
|
||||
color: var(--m-red);
|
||||
}
|
||||
}
|
||||
|
||||
.glm-empty-row {
|
||||
text-align: center;
|
||||
padding: 26px;
|
||||
color: var(--m-g500);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--m-g500);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ta-c {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.glm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.glm-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--m-g300);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--m-g700);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
background: var(--m-g50);
|
||||
}
|
||||
}
|
||||
|
||||
.glm-btn-primary {
|
||||
background: var(--m-navy);
|
||||
border-color: var(--m-navy);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #013a75;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.glm-link {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--m-red);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { GlAccount } from 'app/gl-reports/gl-reports-data';
|
||||
import { GlDateRange, glValueForCodes } from 'app/gl-reports/gl-reports.compute';
|
||||
import { RETURN_SCALE, glAccountsForPicker } from './gl-mapping';
|
||||
|
||||
export interface GlMappingDialogData {
|
||||
returnCode: string;
|
||||
lineLabel: string;
|
||||
/** The figure currently printed on the return, in thousands. */
|
||||
filedValue: number | null;
|
||||
selected: string[];
|
||||
range: GlDateRange;
|
||||
branchCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets a user pin one or more GL accounts to a single return line, showing live what
|
||||
* the ledger would produce for the selection against the figure currently on the return.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-gl-mapping-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
FaIconComponent
|
||||
],
|
||||
templateUrl: './gl-mapping-dialog.component.html',
|
||||
styleUrls: ['./gl-mapping-dialog.component.scss']
|
||||
})
|
||||
export class GlMappingDialogComponent {
|
||||
private dialogRef = inject(MatDialogRef<GlMappingDialogComponent>);
|
||||
data = inject<GlMappingDialogData>(MAT_DIALOG_DATA);
|
||||
|
||||
readonly accounts: GlAccount[] = glAccountsForPicker();
|
||||
search = '';
|
||||
typeFilter = '';
|
||||
selected: string[] = [];
|
||||
|
||||
readonly types = [
|
||||
'ASSET',
|
||||
'LIABILITY',
|
||||
'INCOME',
|
||||
'EXPENSE',
|
||||
'CONTINGENT'
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.selected = [...(this.data.selected ?? [])];
|
||||
}
|
||||
|
||||
get filtered(): GlAccount[] {
|
||||
const q = this.search.trim().toLowerCase();
|
||||
return this.accounts.filter((a) => {
|
||||
if (this.typeFilter && a.type !== this.typeFilter) {
|
||||
return false;
|
||||
}
|
||||
if (!q) {
|
||||
return true;
|
||||
}
|
||||
return a.code.includes(q) || a.name.toLowerCase().includes(q) || a.group.toLowerCase().includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
isSelected(code: string): boolean {
|
||||
return this.selected.includes(code);
|
||||
}
|
||||
|
||||
toggle(code: string): void {
|
||||
this.selected = this.isSelected(code) ? this.selected.filter((c) => c !== code) : [
|
||||
...this.selected,
|
||||
code
|
||||
];
|
||||
}
|
||||
|
||||
removeSelected(code: string): void {
|
||||
this.selected = this.selected.filter((c) => c !== code);
|
||||
}
|
||||
|
||||
clearAll(): void {
|
||||
this.selected = [];
|
||||
}
|
||||
|
||||
accountName(code: string): string {
|
||||
return this.accounts.find((a) => a.code === code)?.name ?? code;
|
||||
}
|
||||
|
||||
/** Live ledger figure for the current selection, in the return's scale. */
|
||||
get derivedValue(): number {
|
||||
return glValueForCodes(this.selected, this.data.range, this.data.branchCode) / RETURN_SCALE;
|
||||
}
|
||||
|
||||
get variance(): number | null {
|
||||
return this.data.filedValue == null ? null : this.derivedValue - this.data.filedValue;
|
||||
}
|
||||
|
||||
get matchesFiled(): boolean {
|
||||
return this.variance !== null && Math.abs(this.variance) < 0.005;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.dialogRef.close(this.selected);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(undefined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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 { inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { BranchScopeService } from 'app/core/branch-scope.service';
|
||||
import { GlDateRange } from 'app/gl-reports/gl-reports.compute';
|
||||
import { GL_BRANCHES } from 'app/gl-reports/gl-reports-data';
|
||||
import { GlMappingDialogComponent, GlMappingDialogData } from './gl-mapping-dialog.component';
|
||||
import {
|
||||
clearReturnMappings,
|
||||
getMapping,
|
||||
lineKeyFor,
|
||||
mappedLineCount,
|
||||
mappedValue,
|
||||
removeGl,
|
||||
setMapping
|
||||
} from './gl-mapping';
|
||||
|
||||
/**
|
||||
* Shared GL-mapping behaviour for the OSS return screens.
|
||||
*
|
||||
* Each return line can be pinned to GL accounts; where it is, the figure shown comes
|
||||
* from the ledger rather than the static filed value. Subclasses only supply their
|
||||
* return code — everything else, including the ledger period and branch scope, is
|
||||
* common.
|
||||
*/
|
||||
export abstract class GlMappingHost {
|
||||
private dialog = inject(MatDialog);
|
||||
private branchScope = inject(BranchScopeService);
|
||||
|
||||
/** e.g. 'R070'. Used to namespace this return's mappings. */
|
||||
abstract readonly glReturnCode: string;
|
||||
|
||||
/** Ledger period the mapped figures are drawn from. */
|
||||
abstract get glRange(): GlDateRange;
|
||||
|
||||
/** Whether the per-line mapping controls are visible. */
|
||||
glMapMode = false;
|
||||
|
||||
/** '' = all branches consolidated. Seeded from the login's own scope. */
|
||||
glBranchCode = this.branchScope.defaultBranchCode;
|
||||
readonly glBranches = GL_BRANCHES;
|
||||
|
||||
/** Head Office may derive figures from any branch; a branch login cannot. */
|
||||
get canSelectGlBranch(): boolean {
|
||||
return this.branchScope.canSelectBranch;
|
||||
}
|
||||
get glScopeName(): string {
|
||||
return this.branchScope.scopeLabel(this.glBranchCode);
|
||||
}
|
||||
|
||||
/** Branch actually used for every ledger lookup, enforced rather than trusted. */
|
||||
private get effectiveGlBranch(): string {
|
||||
return this.branchScope.enforce(this.glBranchCode);
|
||||
}
|
||||
|
||||
toggleGlMapMode(): void {
|
||||
this.glMapMode = !this.glMapMode;
|
||||
}
|
||||
|
||||
glLineKey(label: string): string {
|
||||
return lineKeyFor(label);
|
||||
}
|
||||
|
||||
glCodesFor(label: string): string[] {
|
||||
return getMapping(this.glReturnCode, this.glLineKey(label));
|
||||
}
|
||||
|
||||
isGlMapped(label: string): boolean {
|
||||
return this.glCodesFor(label).length > 0;
|
||||
}
|
||||
|
||||
get glMappedCount(): number {
|
||||
return mappedLineCount(this.glReturnCode);
|
||||
}
|
||||
|
||||
/** Ledger figure for a line, or null when it has no mapping. */
|
||||
glValue(label: string): number | null {
|
||||
return mappedValue(this.glReturnCode, this.glLineKey(label), this.glRange, this.effectiveGlBranch);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number to print for a line: the ledger value when mapped, otherwise the
|
||||
* value filed on the return.
|
||||
*/
|
||||
effectiveValue(label: string, filed: number | null): number | null {
|
||||
const mapped = this.glValue(label);
|
||||
return mapped === null ? filed : mapped;
|
||||
}
|
||||
|
||||
/** True when a mapped line disagrees with the figure originally filed. */
|
||||
glDiffers(label: string, filed: number | null): boolean {
|
||||
const mapped = this.glValue(label);
|
||||
if (mapped === null || filed === null) {
|
||||
return false;
|
||||
}
|
||||
return Math.abs(mapped - filed) >= 0.005;
|
||||
}
|
||||
|
||||
removeGlFromLine(label: string, code: string): void {
|
||||
removeGl(this.glReturnCode, this.glLineKey(label), code);
|
||||
}
|
||||
|
||||
clearAllGlMappings(): void {
|
||||
clearReturnMappings(this.glReturnCode);
|
||||
}
|
||||
|
||||
/** Opens the chart-of-accounts picker for one return line. */
|
||||
openGlPicker(label: string, filed: number | null): void {
|
||||
const key = this.glLineKey(label);
|
||||
const data: GlMappingDialogData = {
|
||||
returnCode: this.glReturnCode,
|
||||
lineLabel: label,
|
||||
filedValue: filed,
|
||||
selected: getMapping(this.glReturnCode, key),
|
||||
range: this.glRange,
|
||||
branchCode: this.effectiveGlBranch
|
||||
};
|
||||
this.dialog
|
||||
.open(GlMappingDialogComponent, {
|
||||
width: '92vw',
|
||||
maxWidth: '980px',
|
||||
maxHeight: '92vh',
|
||||
autoFocus: false,
|
||||
data
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((codes: string[] | undefined) => {
|
||||
if (codes) {
|
||||
setMapping(this.glReturnCode, key, codes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps RBI return line items to General Ledger accounts.
|
||||
*
|
||||
* A return line can be pinned to one or more GL codes; the line's figure is then
|
||||
* derived from the ledger instead of being a static number, so the filed return and
|
||||
* the books cannot drift apart silently. Mappings are keyed by return code plus a
|
||||
* stable line key and persisted locally.
|
||||
*
|
||||
* The OSS returns are denominated in thousands while the ledger is in rupees, so
|
||||
* every derived figure is scaled on the way out.
|
||||
*/
|
||||
|
||||
import { GL_ACCOUNTS, GlAccount } from 'app/gl-reports/gl-reports-data';
|
||||
import { GlDateRange, glValueForCodes } from 'app/gl-reports/gl-reports.compute';
|
||||
|
||||
const STORAGE_KEY = 'rbi_return_gl_mappings';
|
||||
|
||||
/** Returns are reported in thousands of rupees. */
|
||||
export const RETURN_SCALE = 1000;
|
||||
|
||||
export type GlMappingStore = Record<string, string[]>;
|
||||
|
||||
function mapKey(returnCode: string, lineKey: string): string {
|
||||
return `${returnCode}::${lineKey}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The store is read from template getters on every change-detection pass, so it is
|
||||
* held in memory and only re-parsed when a write invalidates it. Parsing JSON a few
|
||||
* hundred times per click is otherwise a measurable cost on the larger returns.
|
||||
*/
|
||||
let storeCache: GlMappingStore | null = null;
|
||||
|
||||
function readStore(): GlMappingStore {
|
||||
if (storeCache) {
|
||||
return storeCache;
|
||||
}
|
||||
try {
|
||||
storeCache = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') as GlMappingStore;
|
||||
} catch {
|
||||
storeCache = {};
|
||||
}
|
||||
return storeCache;
|
||||
}
|
||||
|
||||
function writeStore(store: GlMappingStore): void {
|
||||
storeCache = store;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
||||
}
|
||||
|
||||
/** GL codes currently pinned to a return line. */
|
||||
export function getMapping(returnCode: string, lineKey: string): string[] {
|
||||
return readStore()[mapKey(returnCode, lineKey)] ?? [];
|
||||
}
|
||||
|
||||
export function setMapping(returnCode: string, lineKey: string, codes: string[]): void {
|
||||
const store = readStore();
|
||||
const key = mapKey(returnCode, lineKey);
|
||||
if (codes.length) {
|
||||
store[key] = codes;
|
||||
} else {
|
||||
delete store[key];
|
||||
}
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
export function addGl(returnCode: string, lineKey: string, code: string): void {
|
||||
const current = getMapping(returnCode, lineKey);
|
||||
if (!current.includes(code)) {
|
||||
setMapping(returnCode, lineKey, [
|
||||
...current,
|
||||
code
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeGl(returnCode: string, lineKey: string, code: string): void {
|
||||
setMapping(
|
||||
returnCode,
|
||||
lineKey,
|
||||
getMapping(returnCode, lineKey).filter((c) => c !== code)
|
||||
);
|
||||
}
|
||||
|
||||
/** Every line in a return that currently has a mapping. */
|
||||
export function mappedLineCount(returnCode: string): number {
|
||||
const prefix = `${returnCode}::`;
|
||||
return Object.keys(readStore()).filter((k) => k.startsWith(prefix)).length;
|
||||
}
|
||||
|
||||
export function clearReturnMappings(returnCode: string): void {
|
||||
const store = readStore();
|
||||
const prefix = `${returnCode}::`;
|
||||
for (const k of Object.keys(store)) {
|
||||
if (k.startsWith(prefix)) {
|
||||
delete store[k];
|
||||
}
|
||||
}
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ledger value for a mapped line, expressed in the return's own scale (thousands).
|
||||
* Returns null when the line has no mapping, so callers can fall back to the filed figure.
|
||||
*/
|
||||
export function mappedValue(returnCode: string, lineKey: string, range: GlDateRange, branchCode = ''): number | null {
|
||||
const codes = getMapping(returnCode, lineKey);
|
||||
if (!codes.length) {
|
||||
return null;
|
||||
}
|
||||
return glValueForCodes(codes, range, branchCode) / RETURN_SCALE;
|
||||
}
|
||||
|
||||
/** A line key that stays stable across reorderings — derived from the label itself. */
|
||||
export function lineKeyFor(label: string): string {
|
||||
return label
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 80);
|
||||
}
|
||||
|
||||
export interface GlMappingEntry {
|
||||
returnCode: string;
|
||||
lineKey: string;
|
||||
codes: string[];
|
||||
}
|
||||
|
||||
/** Every mapping across all returns — drives the GL Mapping for Report listing. */
|
||||
export function allMappings(): GlMappingEntry[] {
|
||||
return Object.entries(readStore()).map(
|
||||
([
|
||||
key,
|
||||
codes
|
||||
]) => {
|
||||
const [
|
||||
returnCode,
|
||||
lineKey
|
||||
] = key.split('::');
|
||||
return { returnCode, lineKey, codes };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Human-readable name for a return code. */
|
||||
export function returnName(code: string): string {
|
||||
const names: Record<string, string> = {
|
||||
R070: 'OSS 1 — Return on Assets and Liabilities',
|
||||
R071: 'OSS 2 — Return on Earnings',
|
||||
R323: 'OSS 3 — Return on Asset Quality'
|
||||
};
|
||||
return names[code] ?? code;
|
||||
}
|
||||
|
||||
export function glAccountsForPicker(): GlAccount[] {
|
||||
return GL_ACCOUNTS;
|
||||
}
|
||||
|
||||
export function glLabel(code: string): string {
|
||||
const a = GL_ACCOUNTS.find((x) => x.code === code);
|
||||
return a ? `${a.code} — ${a.name}` : code;
|
||||
}
|
||||
@@ -360,6 +360,158 @@ td.derived {
|
||||
color: var(--o-g500);
|
||||
}
|
||||
|
||||
/* ── General Ledger mapping ─────────────────────────────────────────────── */
|
||||
.oss-gl-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--o-g200);
|
||||
|
||||
&.on {
|
||||
background: var(--o-blue-l);
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-gl-bar-l {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
font-size: 13px;
|
||||
color: var(--o-g700);
|
||||
}
|
||||
|
||||
.oss-gl-bar-r {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.oss-gl-count {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
background: var(--o-green-l);
|
||||
color: var(--o-green);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.oss-gl-inp {
|
||||
padding: 7px 11px;
|
||||
border: 1px solid var(--o-g300);
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px;
|
||||
font-family: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.oss-btn-clear {
|
||||
color: var(--o-red);
|
||||
border-color: var(--o-red);
|
||||
}
|
||||
|
||||
.oss-gl-col {
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
/* Read-only branch shown to a branch-level login in place of the selector. */
|
||||
.oss-gl-scope {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid var(--o-g300);
|
||||
border-radius: 8px;
|
||||
background: var(--o-g100);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--o-g700);
|
||||
}
|
||||
|
||||
.oss-gl-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.oss-gl-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 4px 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #e0f2fe;
|
||||
color: #075985;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oss-gl-chip-x {
|
||||
border: none;
|
||||
background: rgb(7 89 133 / 14%);
|
||||
color: #075985;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
|
||||
&:hover {
|
||||
background: var(--o-red);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-gl-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 9px;
|
||||
border: 1px dashed var(--o-g300);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--o-g500);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--o-navy);
|
||||
color: var(--o-navy);
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-gl-warn {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
color: var(--o-amber-d);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A figure sourced from the ledger rather than the filed return. */
|
||||
td.gl-sourced {
|
||||
background: #ecfdf5;
|
||||
box-shadow: inset 3px 0 0 var(--o-green);
|
||||
}
|
||||
|
||||
td.gl-diff {
|
||||
background: #fffbeb;
|
||||
box-shadow: inset 3px 0 0 #d97706;
|
||||
}
|
||||
|
||||
/* Tie-up status pills (cross-return reconciliation checks). */
|
||||
.report-pill {
|
||||
display: inline-block;
|
||||
|
||||
@@ -7,13 +7,7 @@
|
||||
*/
|
||||
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import {
|
||||
MatrixTable,
|
||||
ReturnGeneralInfoRow,
|
||||
ReturnLine,
|
||||
ReturnSignatory,
|
||||
SIGNATORY_FIELDS
|
||||
} from './oss-return.model';
|
||||
import { MatrixTable, ReturnGeneralInfoRow, ReturnLine, ReturnSignatory, SIGNATORY_FIELDS } from './oss-return.model';
|
||||
|
||||
/** Palette lifted from the RBI CIMS workbooks so exports look like the originals. */
|
||||
export const XL = {
|
||||
@@ -45,12 +39,7 @@ export function addBanner(sheet: ExcelJS.Worksheet, title: string, lastCol: stri
|
||||
sheet.addRow([]);
|
||||
}
|
||||
|
||||
export function boxAllCells(
|
||||
sheet: ExcelJS.Worksheet,
|
||||
from: number,
|
||||
to: number,
|
||||
lastColIndex: number
|
||||
): void {
|
||||
export function boxAllCells(sheet: ExcelJS.Worksheet, from: number, to: number, lastColIndex: number): void {
|
||||
for (let r = from; r <= to; r++) {
|
||||
for (let c = 1; c <= lastColIndex; c++) {
|
||||
sheet.getRow(r).getCell(c).border = {
|
||||
@@ -64,7 +53,10 @@ export function boxAllCells(
|
||||
}
|
||||
|
||||
export function addSectionHeader(sheet: ExcelJS.Worksheet, label: string, unitLabel?: string): void {
|
||||
const row = sheet.addRow([label, unitLabel ?? '']);
|
||||
const row = sheet.addRow([
|
||||
label,
|
||||
unitLabel ?? ''
|
||||
]);
|
||||
row.font = { bold: true };
|
||||
row.eachCell((cell) => (cell.fill = solidFill(XL.headerGrey)));
|
||||
row.getCell(2).alignment = { horizontal: 'right' };
|
||||
@@ -73,7 +65,10 @@ export function addSectionHeader(sheet: ExcelJS.Worksheet, label: string, unitLa
|
||||
/** Renders a labelled block; indents by level and shades derived totals. */
|
||||
export function addLineRows(sheet: ExcelJS.Worksheet, lines: ReturnLine[]): void {
|
||||
for (const line of lines) {
|
||||
const row = sheet.addRow([line.label, line.value]);
|
||||
const row = sheet.addRow([
|
||||
line.label,
|
||||
line.value
|
||||
]);
|
||||
const labelCell = row.getCell(1);
|
||||
const valueCell = row.getCell(2);
|
||||
|
||||
@@ -98,7 +93,10 @@ export function addMatrixTable(sheet: ExcelJS.Worksheet, table: MatrixTable): vo
|
||||
const colCount = table.columns.length + 1;
|
||||
const start = sheet.rowCount + 1;
|
||||
|
||||
const titleRow = sheet.addRow([table.title, ...(table.unit ? [table.unit] : [])]);
|
||||
const titleRow = sheet.addRow([
|
||||
table.title,
|
||||
...(table.unit ? [table.unit] : [])
|
||||
]);
|
||||
if (!table.unit) {
|
||||
sheet.mergeCells(titleRow.number, 1, titleRow.number, colCount);
|
||||
}
|
||||
@@ -115,13 +113,19 @@ export function addMatrixTable(sheet: ExcelJS.Worksheet, table: MatrixTable): vo
|
||||
sub.getCell(1).fill = solidFill(XL.headerGrey);
|
||||
}
|
||||
|
||||
const head = sheet.addRow(['Particulars', ...table.columns]);
|
||||
const head = sheet.addRow([
|
||||
'Particulars',
|
||||
...table.columns
|
||||
]);
|
||||
head.font = { bold: true };
|
||||
head.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
|
||||
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
|
||||
for (const row of table.rows) {
|
||||
const excelRow = sheet.addRow([row.label, ...row.values]);
|
||||
const excelRow = sheet.addRow([
|
||||
row.label,
|
||||
...row.values
|
||||
]);
|
||||
const labelCell = excelRow.getCell(1);
|
||||
labelCell.alignment = { wrapText: true, vertical: 'middle', indent: row.level };
|
||||
if (row.level === 0 || row.header) {
|
||||
@@ -158,13 +162,15 @@ export function buildGeneralInfoSheet(wb: ExcelJS.Workbook, rows: ReturnGeneralI
|
||||
const sheet = wb.addWorksheet('General Information');
|
||||
sheet.columns = [
|
||||
{ width: 26 },
|
||||
{ width: 58 }
|
||||
];
|
||||
{ width: 58 }];
|
||||
addBanner(sheet, 'General Information', 'B');
|
||||
|
||||
const first = sheet.rowCount + 1;
|
||||
for (const item of rows) {
|
||||
const row = sheet.addRow([item.label, item.value]);
|
||||
const row = sheet.addRow([
|
||||
item.label,
|
||||
item.value
|
||||
]);
|
||||
row.getCell(1).fill = solidFill(XL.keyCell);
|
||||
// The workbook tints the audit flag yellow and leaves unfilled cells grey.
|
||||
const fill = item.label === 'Audit Flag' ? XL.auditYellow : item.value ? XL.derivedBlue : XL.headerGrey;
|
||||
@@ -182,19 +188,24 @@ export function buildSignatorySheet(
|
||||
const sheet = wb.addWorksheet(sheetName);
|
||||
sheet.columns = [
|
||||
{ width: 44 },
|
||||
{ width: 24 }
|
||||
];
|
||||
{ width: 24 }];
|
||||
addBanner(sheet, sheetName, 'B');
|
||||
|
||||
for (const person of signatories) {
|
||||
const start = sheet.rowCount + 1;
|
||||
const head = sheet.addRow([person.heading, '']);
|
||||
const head = sheet.addRow([
|
||||
person.heading,
|
||||
''
|
||||
]);
|
||||
head.font = { bold: true };
|
||||
head.alignment = { horizontal: 'center' };
|
||||
head.getCell(1).fill = solidFill(XL.keyCell);
|
||||
|
||||
for (const f of SIGNATORY_FIELDS) {
|
||||
const row = sheet.addRow([person[f.key], f.caption]);
|
||||
const row = sheet.addRow([
|
||||
person[f.key],
|
||||
f.caption
|
||||
]);
|
||||
row.getCell(2).font = { bold: true };
|
||||
row.getCell(2).alignment = { horizontal: 'center' };
|
||||
row.getCell(2).fill = solidFill(XL.keyCell);
|
||||
|
||||
@@ -153,8 +153,7 @@ export const RAL_LIABILITIES: RalLine[] = [
|
||||
{ label: 'ii. Borrowings', level: 1, value: null },
|
||||
|
||||
{
|
||||
label:
|
||||
'12. Risk Provisions Other than netted off assets (BDDR, Spl BDDR, Depreciation on fixed assets etc.,)',
|
||||
label: '12. Risk Provisions Other than netted off assets (BDDR, Spl BDDR, Depreciation on fixed assets etc.,)',
|
||||
level: 0,
|
||||
value: 39440,
|
||||
total: true
|
||||
@@ -192,7 +191,12 @@ export const RAL_ASSETS: RalLine[] = [
|
||||
|
||||
{ label: '2. I. Due from Banks', level: 0, value: 57600, total: true },
|
||||
{ label: 'A. Balances with Banks', level: 1, value: 57600, total: true },
|
||||
{ label: 'i. State/ Central Cooperative Bank of the State/District concerned in', level: 2, value: 32268, total: true },
|
||||
{
|
||||
label: 'i. State/ Central Cooperative Bank of the State/District concerned in',
|
||||
level: 2,
|
||||
value: 32268,
|
||||
total: true
|
||||
},
|
||||
{ label: 'a. Current', level: 2, value: 5980 },
|
||||
{ label: 'b. Savings', level: 2, value: null },
|
||||
{ label: 'c. Fixed', level: 2, value: 26288 },
|
||||
@@ -353,26 +357,73 @@ export const RAL_AGEING_HEADERS: string[] = [
|
||||
|
||||
/** Sheet 3 — Annexure 1 :: 3. Age-wise Inter Branch Adjustment Account */
|
||||
export const RAL_INTER_BRANCH_AGEING: RalAgeingRow[] = [
|
||||
{ period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{ period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{ period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{ period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{
|
||||
period: '3 months to 6 months',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{
|
||||
period: 'Above 6 months to 1 year',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{
|
||||
period: 'Above 1 year to 2 years',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{
|
||||
period: 'Above 2 years',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{ period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 }
|
||||
];
|
||||
|
||||
/** Sheet 3 — Annexure 1 :: 4. Age-wise Inter Bank Reconciliation Account */
|
||||
export const RAL_INTER_BANK_AGEING: RalAgeingRow[] = [
|
||||
{ period: '3 months to 6 months', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{ period: 'Above 6 months to 1 year', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{ period: 'Above 1 year to 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{ period: 'Above 2 years', noOfDebitEntries: null, amtOfDebitEntries: null, noOfCreditEntries: null, amtOfCreditEntries: null },
|
||||
{
|
||||
period: '3 months to 6 months',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{
|
||||
period: 'Above 6 months to 1 year',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{
|
||||
period: 'Above 1 year to 2 years',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{
|
||||
period: 'Above 2 years',
|
||||
noOfDebitEntries: null,
|
||||
amtOfDebitEntries: null,
|
||||
noOfCreditEntries: null,
|
||||
amtOfCreditEntries: null
|
||||
},
|
||||
{ period: 'Total', noOfDebitEntries: 0, amtOfDebitEntries: 0, noOfCreditEntries: 0, amtOfCreditEntries: 0 }
|
||||
];
|
||||
|
||||
/** Sheet 3 — Annexure 1 :: 5. Unsecured guarantees issued by the bank */
|
||||
export const RAL_UNSECURED_GUARANTEES: RalUnsecuredGuarantee[] = [
|
||||
{ srNo: 1, borrower: '', beneficiary: '', amountOutstanding: null }
|
||||
];
|
||||
{ srNo: 1, borrower: '', beneficiary: '', amountOutstanding: null }];
|
||||
export const RAL_UNSECURED_GUARANTEES_TOTAL = 0;
|
||||
|
||||
/** Sheet 4 — Annexure 2 :: Sundry Debtors ageing buckets */
|
||||
@@ -401,20 +452,49 @@ export const RAL_SUNDRY_DEBTORS_NOTE =
|
||||
export const RAL_DEPOSITORS_BORROWERS: RalLine[] = [
|
||||
{ label: '1.Total No of Depositors', level: 0, value: 8156, total: true, count: true },
|
||||
{ label: 'Total Customer Deposits', level: 0, value: 634309, total: true },
|
||||
{ label: 'a. i) Of which Total No. of Depositors with Deposits less than Rs. 1 Lakh', level: 1, value: 6568, count: true },
|
||||
{
|
||||
label: 'a. i) Of which Total No. of Depositors with Deposits less than Rs. 1 Lakh',
|
||||
level: 1,
|
||||
value: 6568,
|
||||
count: true
|
||||
},
|
||||
{ label: 'ii) Of which total Deposit Amount', level: 1, value: 109992 },
|
||||
{ label: 'b. i) Of which Total No. of Depositors with Deposits Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 739, count: true },
|
||||
{
|
||||
label: 'b. i) Of which Total No. of Depositors with Deposits Between Rs 1 Lakh to Rs. 2 Lakh',
|
||||
level: 1,
|
||||
value: 739,
|
||||
count: true
|
||||
},
|
||||
{ label: 'ii) Of which total Deposit Amount', level: 1, value: 102475 },
|
||||
{ label: 'c. i) Of which Total No. of Depositors with Deposits Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 580, count: true },
|
||||
{
|
||||
label: 'c. i) Of which Total No. of Depositors with Deposits Between Rs. 2 Lakh to Rs 5 Lakh',
|
||||
level: 1,
|
||||
value: 580,
|
||||
count: true
|
||||
},
|
||||
{ label: 'ii) Of which total Deposit Amount', level: 1, value: 176820 },
|
||||
|
||||
{ label: '2.Total No of Borrowers', level: 0, value: 363, total: true, count: true },
|
||||
{ label: 'Total Loans and Advances (Other than Banks)', level: 0, value: 391288, total: true },
|
||||
{ label: 'a. i) Of which Total No. of Borrowers with Loans and Advances less than Rs. 1 Lakh', level: 1, value: 55, count: true },
|
||||
{
|
||||
label: 'a. i) Of which Total No. of Borrowers with Loans and Advances less than Rs. 1 Lakh',
|
||||
level: 1,
|
||||
value: 55,
|
||||
count: true
|
||||
},
|
||||
{ label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2061 },
|
||||
{ label: 'b. i) Of which Total No. of Borrowers with Loans and Advances Between Rs 1 Lakh to Rs. 2 Lakh', level: 1, value: 18, count: true },
|
||||
{
|
||||
label: 'b. i) Of which Total No. of Borrowers with Loans and Advances Between Rs 1 Lakh to Rs. 2 Lakh',
|
||||
level: 1,
|
||||
value: 18,
|
||||
count: true
|
||||
},
|
||||
{ label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 2837 },
|
||||
{ label: 'c. i) Of which Total No. of Borrowers with Loans and Advances Between Rs. 2 Lakh to Rs 5 Lakh', level: 1, value: 55, count: true },
|
||||
{
|
||||
label: 'c. i) Of which Total No. of Borrowers with Loans and Advances Between Rs. 2 Lakh to Rs 5 Lakh',
|
||||
level: 1,
|
||||
value: 55,
|
||||
count: true
|
||||
},
|
||||
{ label: 'ii) Of which total Loans and Advances Amount', level: 1, value: 21260 }
|
||||
];
|
||||
|
||||
|
||||
@@ -81,11 +81,16 @@
|
||||
<div class="oss-section">
|
||||
<div class="oss-sheet-banner">Assets & Liabilities</div>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="glBar"></ng-container>
|
||||
|
||||
<div class="oss-tbl-wrap">
|
||||
<table class="oss-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Liabilities</th>
|
||||
@if (glMapMode) {
|
||||
<th class="oss-gl-col">Mapped GL accounts</th>
|
||||
}
|
||||
<th class="ta-r">Rs in Thousands</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -93,8 +98,25 @@
|
||||
@for (line of liabilities; track $index) {
|
||||
<tr [class.total-row]="line.total">
|
||||
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
|
||||
<td class="ta-r mono" [class.derived]="line.total">
|
||||
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
|
||||
@if (glMapMode) {
|
||||
<td>
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="glCell"
|
||||
[ngTemplateOutletContext]="{ label: line.label, filed: line.value }"
|
||||
></ng-container>
|
||||
</td>
|
||||
}
|
||||
<td
|
||||
class="ta-r mono"
|
||||
[class.derived]="line.total"
|
||||
[class.gl-sourced]="isGlMapped(line.label)"
|
||||
[class.gl-diff]="glDiffers(line.label, line.value)"
|
||||
>
|
||||
{{
|
||||
effectiveValue(line.label, line.value) === null
|
||||
? ''
|
||||
: (effectiveValue(line.label, line.value) | number: (line.count ? '1.0-0' : '1.2-2'))
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -107,6 +129,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Assets</th>
|
||||
@if (glMapMode) {
|
||||
<th class="oss-gl-col">Mapped GL accounts</th>
|
||||
}
|
||||
<th class="ta-r">Rs in Thousands</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -114,8 +139,25 @@
|
||||
@for (line of assets; track $index) {
|
||||
<tr [class.total-row]="line.total">
|
||||
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
|
||||
<td class="ta-r mono" [class.derived]="line.total">
|
||||
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
|
||||
@if (glMapMode) {
|
||||
<td>
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="glCell"
|
||||
[ngTemplateOutletContext]="{ label: line.label, filed: line.value }"
|
||||
></ng-container>
|
||||
</td>
|
||||
}
|
||||
<td
|
||||
class="ta-r mono"
|
||||
[class.derived]="line.total"
|
||||
[class.gl-sourced]="isGlMapped(line.label)"
|
||||
[class.gl-diff]="glDiffers(line.label, line.value)"
|
||||
>
|
||||
{{
|
||||
effectiveValue(line.label, line.value) === null
|
||||
? ''
|
||||
: (effectiveValue(line.label, line.value) | number: (line.count ? '1.0-0' : '1.2-2'))
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -343,7 +385,7 @@
|
||||
<tr [class.total-row]="line.total">
|
||||
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
|
||||
<td class="ta-r mono" [class.derived]="line.total">
|
||||
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
|
||||
{{ line.value === null ? '' : (line.value | number: (line.count ? '1.0-0' : '1.2-2')) }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -417,6 +459,68 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- GL mapping toolbar, shown above any mappable statement -->
|
||||
<ng-template #glBar>
|
||||
<div class="oss-gl-bar" [class.on]="glMapMode">
|
||||
<div class="oss-gl-bar-l">
|
||||
<fa-icon icon="project-diagram"></fa-icon>
|
||||
<span>
|
||||
<strong>General Ledger mapping</strong>
|
||||
@if (glMappedCount) {
|
||||
<span class="oss-gl-count">{{ glMappedCount }} line(s) mapped</span>
|
||||
} @else {
|
||||
<span class="muted"> — pin GL accounts to a line and its figure comes from the ledger</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="oss-gl-bar-r">
|
||||
@if (glMapMode) {
|
||||
@if (canSelectGlBranch) {
|
||||
<select class="oss-gl-inp" [(ngModel)]="glBranchCode">
|
||||
<option value="">All branches</option>
|
||||
@for (b of glBranches; track b.code) {
|
||||
<option [value]="b.code">{{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
<span class="oss-gl-scope" title="Your login is restricted to this branch">
|
||||
<fa-icon icon="university"></fa-icon> {{ glScopeName }}
|
||||
</span>
|
||||
}
|
||||
@if (glMappedCount) {
|
||||
<button class="oss-btn oss-btn-clear" (click)="clearAllGlMappings()">Clear all</button>
|
||||
}
|
||||
}
|
||||
<button class="oss-btn" [class.oss-btn-xls]="!glMapMode" (click)="toggleGlMapMode()">
|
||||
<fa-icon [icon]="glMapMode ? 'times' : 'plus'"></fa-icon>
|
||||
{{ glMapMode ? 'Done' : 'Add / Remove GLs' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- Per-line GL chips and picker trigger -->
|
||||
<ng-template #glCell let-label="label" let-filed="filed">
|
||||
<div class="oss-gl-cell">
|
||||
@for (code of glCodesFor(label); track code) {
|
||||
<span class="oss-gl-chip">
|
||||
<span class="mono">{{ code }}</span>
|
||||
<button class="oss-gl-chip-x" (click)="removeGlFromLine(label, code)" [attr.aria-label]="'Remove ' + code">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
<button class="oss-gl-add" (click)="openGlPicker(label, filed)">
|
||||
<fa-icon icon="plus"></fa-icon> {{ glCodesFor(label).length ? 'Edit' : 'Add GL' }}
|
||||
</button>
|
||||
@if (glDiffers(label, filed)) {
|
||||
<span class="oss-gl-warn" title="Ledger figure differs from the value filed on the return">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon> differs from filed
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- TOAST -->
|
||||
<div class="oss-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { GlMappingHost } from '../gl-mapping.host';
|
||||
import {
|
||||
addBanner,
|
||||
addLineRows,
|
||||
@@ -55,7 +56,14 @@ type Oss1Tab = 'general' | 'balance-sheet' | 'annexure1' | 'annexure2' | 'signat
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class Oss1RalComponent {
|
||||
export class Oss1RalComponent extends GlMappingHost {
|
||||
readonly glReturnCode = 'R070';
|
||||
|
||||
/** RAL covers the reporting quarter; mapped figures are drawn from the same span. */
|
||||
get glRange() {
|
||||
return { from: '2026-04-01', to: '2026-06-30' };
|
||||
}
|
||||
|
||||
activeTab: Oss1Tab = 'general';
|
||||
|
||||
generalInfo = RAL_GENERAL_INFO;
|
||||
@@ -146,8 +154,7 @@ export class Oss1RalComponent {
|
||||
const sheet = wb.addWorksheet('Assets & Liabilities');
|
||||
sheet.columns = [
|
||||
{ width: 78 },
|
||||
{ width: 20 }
|
||||
];
|
||||
{ width: 20 }];
|
||||
addBanner(sheet, 'Assets & Liabilities', 'B');
|
||||
|
||||
const first = sheet.rowCount + 1;
|
||||
@@ -187,18 +194,25 @@ export class Oss1RalComponent {
|
||||
{ width: 16 },
|
||||
{ width: 12 },
|
||||
{ width: 12 },
|
||||
{ width: 12 }
|
||||
];
|
||||
{ width: 12 }];
|
||||
addBanner(sheet, 'Annexure 1', 'H');
|
||||
|
||||
// 1. Break-up of Other Funds and Reserves
|
||||
let start = sheet.rowCount + 1;
|
||||
const fundsTitle = sheet.addRow(['1. Break-up of Other Funds and Reserves', '', 'Rs in Thousands']);
|
||||
const fundsTitle = sheet.addRow([
|
||||
'1. Break-up of Other Funds and Reserves',
|
||||
'',
|
||||
'Rs in Thousands'
|
||||
]);
|
||||
sheet.mergeCells(`A${fundsTitle.number}:B${fundsTitle.number}`);
|
||||
fundsTitle.font = { bold: true };
|
||||
fundsTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
for (const line of this.otherFunds) {
|
||||
const row = sheet.addRow([line.label, '', line.value]);
|
||||
const row = sheet.addRow([
|
||||
line.label,
|
||||
'',
|
||||
line.value
|
||||
]);
|
||||
sheet.mergeCells(`A${row.number}:B${row.number}`);
|
||||
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
|
||||
row.getCell(3).numFmt = '#,##0.00';
|
||||
@@ -281,10 +295,20 @@ export class Oss1RalComponent {
|
||||
gHead.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' };
|
||||
gHead.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
for (const g of this.unsecuredGuarantees) {
|
||||
const row = sheet.addRow([g.srNo, g.borrower, g.beneficiary, g.amountOutstanding]);
|
||||
const row = sheet.addRow([
|
||||
g.srNo,
|
||||
g.borrower,
|
||||
g.beneficiary,
|
||||
g.amountOutstanding
|
||||
]);
|
||||
row.getCell(4).numFmt = '#,##0.00';
|
||||
}
|
||||
const gTotal = sheet.addRow(['Total', '', '', this.unsecuredGuaranteesTotal]);
|
||||
const gTotal = sheet.addRow([
|
||||
'Total',
|
||||
'',
|
||||
'',
|
||||
this.unsecuredGuaranteesTotal
|
||||
]);
|
||||
gTotal.font = { bold: true };
|
||||
gTotal.getCell(4).numFmt = '#,##0.00';
|
||||
gTotal.getCell(4).fill = solidFill(XL.derivedBlue);
|
||||
@@ -327,9 +351,7 @@ export class Oss1RalComponent {
|
||||
3,
|
||||
4,
|
||||
5
|
||||
].forEach(
|
||||
(c) => (row.getCell(c).fill = solidFill(XL.derivedBlue))
|
||||
);
|
||||
].forEach((c) => (row.getCell(c).fill = solidFill(XL.derivedBlue)));
|
||||
}
|
||||
}
|
||||
boxAllCells(sheet, start, sheet.rowCount, 5);
|
||||
@@ -341,8 +363,7 @@ export class Oss1RalComponent {
|
||||
{ width: 10 },
|
||||
{ width: 62 },
|
||||
{ width: 20 },
|
||||
{ width: 20 }
|
||||
];
|
||||
{ width: 20 }];
|
||||
addBanner(sheet, 'Annexure 2', 'D');
|
||||
|
||||
for (const block of this.sundryDebtors) {
|
||||
@@ -357,13 +378,23 @@ export class Oss1RalComponent {
|
||||
s.font = { bold: true, size: 10 };
|
||||
s.getCell(1).fill = solidFill(XL.headerGrey);
|
||||
|
||||
const head = sheet.addRow(['Sr. No', 'Due from Name', 'Amount due', 'Provision made']);
|
||||
const head = sheet.addRow([
|
||||
'Sr. No',
|
||||
'Due from Name',
|
||||
'Amount due',
|
||||
'Provision made'
|
||||
]);
|
||||
head.font = { bold: true };
|
||||
head.alignment = { horizontal: 'center' };
|
||||
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
|
||||
for (const r of block.rows) {
|
||||
const row = sheet.addRow([r.srNo, r.dueFromName, r.amountDue, r.provisionMade]);
|
||||
const row = sheet.addRow([
|
||||
r.srNo,
|
||||
r.dueFromName,
|
||||
r.amountDue,
|
||||
r.provisionMade
|
||||
]);
|
||||
row.getCell(3).numFmt = '#,##0.00';
|
||||
row.getCell(4).numFmt = '#,##0.00';
|
||||
}
|
||||
@@ -378,14 +409,22 @@ export class Oss1RalComponent {
|
||||
|
||||
// Details of Depositors & Borrowers
|
||||
let start = sheet.rowCount + 1;
|
||||
const dbTitle = sheet.addRow(['Details of Depositors & Borrowers', '', 'Rs. In Thousands']);
|
||||
const dbTitle = sheet.addRow([
|
||||
'Details of Depositors & Borrowers',
|
||||
'',
|
||||
'Rs. In Thousands'
|
||||
]);
|
||||
sheet.mergeCells(`A${dbTitle.number}:B${dbTitle.number}`);
|
||||
dbTitle.font = { bold: true };
|
||||
dbTitle.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
dbTitle.getCell(3).alignment = { horizontal: 'right' };
|
||||
|
||||
for (const line of this.depositorsBorrowers) {
|
||||
const row = sheet.addRow([line.label, '', line.value]);
|
||||
const row = sheet.addRow([
|
||||
line.label,
|
||||
'',
|
||||
line.value
|
||||
]);
|
||||
sheet.mergeCells(`A${row.number}:B${row.number}`);
|
||||
row.getCell(1).alignment = { wrapText: true, vertical: 'middle', indent: line.level };
|
||||
row.getCell(3).numFmt = line.count ? '#,##0' : '#,##0.00';
|
||||
@@ -404,7 +443,11 @@ export class Oss1RalComponent {
|
||||
cTitle.font = { bold: true };
|
||||
cTitle.getCell(1).fill = solidFill(XL.headerGrey);
|
||||
for (const line of this.complaints) {
|
||||
const row = sheet.addRow([line.label, '', line.value]);
|
||||
const row = sheet.addRow([
|
||||
line.label,
|
||||
'',
|
||||
line.value
|
||||
]);
|
||||
sheet.mergeCells(`A${row.number}:B${row.number}`);
|
||||
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
|
||||
row.getCell(3).numFmt = '#,##0';
|
||||
|
||||
@@ -122,11 +122,52 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- GL mapping toolbar -->
|
||||
<div class="oss-gl-bar" [class.on]="glMapMode">
|
||||
<div class="oss-gl-bar-l">
|
||||
<fa-icon icon="project-diagram"></fa-icon>
|
||||
<span>
|
||||
<strong>General Ledger mapping</strong>
|
||||
@if (glMappedCount) {
|
||||
<span class="oss-gl-count">{{ glMappedCount }} line(s) mapped</span>
|
||||
} @else {
|
||||
<span class="muted"> — pin GL accounts to a line and its figure comes from the ledger</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="oss-gl-bar-r">
|
||||
@if (glMapMode) {
|
||||
@if (canSelectGlBranch) {
|
||||
<select class="oss-gl-inp" [(ngModel)]="glBranchCode">
|
||||
<option value="">All branches</option>
|
||||
@for (b of glBranches; track b.code) {
|
||||
<option [value]="b.code">{{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
<span class="oss-gl-scope" title="Your login is restricted to this branch">
|
||||
<fa-icon icon="university"></fa-icon> {{ glScopeName }}
|
||||
</span>
|
||||
}
|
||||
@if (glMappedCount) {
|
||||
<button class="oss-btn oss-btn-clear" (click)="clearAllGlMappings()">Clear all</button>
|
||||
}
|
||||
}
|
||||
<button class="oss-btn" [class.oss-btn-xls]="!glMapMode" (click)="toggleGlMapMode()">
|
||||
<fa-icon [icon]="glMapMode ? 'times' : 'plus'"></fa-icon>
|
||||
{{ glMapMode ? 'Done' : 'Add / Remove GLs' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oss-tbl-wrap">
|
||||
<table class="oss-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Particulars</th>
|
||||
@if (glMapMode) {
|
||||
<th class="oss-gl-col">Mapped GL accounts</th>
|
||||
}
|
||||
<th class="ta-r">Rs in Thousand</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -134,8 +175,44 @@
|
||||
@for (line of earnings; track $index) {
|
||||
<tr [class.total-row]="line.total">
|
||||
<td [class]="'lvl-' + line.level">{{ line.label }}</td>
|
||||
<td class="ta-r mono" [class.derived]="line.total">
|
||||
{{ line.value === null ? '' : (line.value | number: line.count ? '1.0-0' : '1.2-2') }}
|
||||
@if (glMapMode) {
|
||||
<td>
|
||||
<div class="oss-gl-cell">
|
||||
@for (code of glCodesFor(line.label); track code) {
|
||||
<span class="oss-gl-chip">
|
||||
<span class="mono">{{ code }}</span>
|
||||
<button
|
||||
class="oss-gl-chip-x"
|
||||
(click)="removeGlFromLine(line.label, code)"
|
||||
[attr.aria-label]="'Remove ' + code"
|
||||
>
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
<button class="oss-gl-add" (click)="openGlPicker(line.label, line.value)">
|
||||
<fa-icon icon="plus"></fa-icon>
|
||||
{{ glCodesFor(line.label).length ? 'Edit' : 'Add GL' }}
|
||||
</button>
|
||||
@if (glDiffers(line.label, line.value)) {
|
||||
<span class="oss-gl-warn" title="Ledger figure differs from the value filed on the return">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon> differs from filed
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td
|
||||
class="ta-r mono"
|
||||
[class.derived]="line.total"
|
||||
[class.gl-sourced]="isGlMapped(line.label)"
|
||||
[class.gl-diff]="glDiffers(line.label, line.value)"
|
||||
>
|
||||
{{
|
||||
effectiveValue(line.label, line.value) === null
|
||||
? ''
|
||||
: (effectiveValue(line.label, line.value) | number: (line.count ? '1.0-0' : '1.2-2'))
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -186,9 +263,10 @@
|
||||
<div class="oss-balance" [class.oss-balance--bad]="!otherExpensesTally">
|
||||
<fa-icon [icon]="otherExpensesTally ? 'check-circle' : 'exclamation-triangle'"></fa-icon>
|
||||
<span>
|
||||
Detail total <strong>₹{{ otherExpensesTotal | number: '1.2-2' }}</strong> vs Earnings line 12 ix)
|
||||
Others
|
||||
<strong>₹{{ otherExpensesLineValue === null ? '—' : (otherExpensesLineValue | number: '1.2-2') }}</strong>
|
||||
Detail total <strong>₹{{ otherExpensesTotal | number: '1.2-2' }}</strong> vs Earnings line 12 ix) Others
|
||||
<strong
|
||||
>₹{{ otherExpensesLineValue === null ? '—' : (otherExpensesLineValue | number: '1.2-2') }}</strong
|
||||
>
|
||||
— {{ otherExpensesTally ? 'tallies' : 'MISMATCH — resolve before filing' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { GlMappingHost } from '../gl-mapping.host';
|
||||
import {
|
||||
addBanner,
|
||||
addLineRows,
|
||||
@@ -54,7 +55,14 @@ interface TieUp {
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class Oss2RoeComponent {
|
||||
export class Oss2RoeComponent extends GlMappingHost {
|
||||
readonly glReturnCode = 'R071';
|
||||
|
||||
/** RoE reports the quarter; mapped income and expense figures use the same span. */
|
||||
get glRange() {
|
||||
return { from: '2026-04-01', to: '2026-06-30' };
|
||||
}
|
||||
|
||||
activeTab: Oss2Tab = 'general';
|
||||
|
||||
generalInfo = ROE_GENERAL_INFO;
|
||||
@@ -178,8 +186,7 @@ export class Oss2RoeComponent {
|
||||
const sheet = wb.addWorksheet('Earnings');
|
||||
sheet.columns = [
|
||||
{ width: 90 },
|
||||
{ width: 20 }
|
||||
];
|
||||
{ width: 20 }];
|
||||
addBanner(sheet, 'Earnings', 'B');
|
||||
|
||||
const first = sheet.rowCount + 1;
|
||||
@@ -205,23 +212,31 @@ export class Oss2RoeComponent {
|
||||
const sheet = wb.addWorksheet('Interest Receivable Not Recog');
|
||||
sheet.columns = [
|
||||
{ width: 72 },
|
||||
{ width: 22 }
|
||||
];
|
||||
{ width: 22 }];
|
||||
addBanner(sheet, 'Interest Receivable Not Recognized', 'B');
|
||||
|
||||
const start = sheet.rowCount + 1;
|
||||
const unit = sheet.addRow(['', 'Rs in Thousand']);
|
||||
const unit = sheet.addRow([
|
||||
'',
|
||||
'Rs in Thousand'
|
||||
]);
|
||||
unit.font = { bold: true };
|
||||
unit.alignment = { horizontal: 'center' };
|
||||
unit.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
|
||||
const head = sheet.addRow(['Interest receivable on NPAs not recognized as income', this.quarterEndDate]);
|
||||
const head = sheet.addRow([
|
||||
'Interest receivable on NPAs not recognized as income',
|
||||
this.quarterEndDate
|
||||
]);
|
||||
head.font = { bold: true };
|
||||
head.getCell(2).alignment = { horizontal: 'center' };
|
||||
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
|
||||
for (const r of this.interestNotRecognized) {
|
||||
const row = sheet.addRow([r.label, r.value]);
|
||||
const row = sheet.addRow([
|
||||
r.label,
|
||||
r.value
|
||||
]);
|
||||
row.getCell(1).alignment = { wrapText: true, vertical: 'middle' };
|
||||
row.getCell(1).fill = solidFill(XL.headerGrey);
|
||||
row.getCell(2).numFmt = '#,##0.00';
|
||||
@@ -238,28 +253,43 @@ export class Oss2RoeComponent {
|
||||
sheet.columns = [
|
||||
{ width: 10 },
|
||||
{ width: 56 },
|
||||
{ width: 20 }
|
||||
];
|
||||
{ width: 20 }];
|
||||
addBanner(sheet, 'Other Operating Expenses', 'C');
|
||||
|
||||
const start = sheet.rowCount + 1;
|
||||
const title = sheet.addRow(['Details of Other Operating Expenses', '', 'Rs in Thousand']);
|
||||
const title = sheet.addRow([
|
||||
'Details of Other Operating Expenses',
|
||||
'',
|
||||
'Rs in Thousand'
|
||||
]);
|
||||
sheet.mergeCells(`A${title.number}:B${title.number}`);
|
||||
title.font = { bold: true };
|
||||
title.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
title.getCell(3).alignment = { horizontal: 'center' };
|
||||
|
||||
const head = sheet.addRow(['Sr No', 'Particulars', 'Amount']);
|
||||
const head = sheet.addRow([
|
||||
'Sr No',
|
||||
'Particulars',
|
||||
'Amount'
|
||||
]);
|
||||
head.font = { bold: true };
|
||||
head.alignment = { horizontal: 'center' };
|
||||
head.eachCell((c) => (c.fill = solidFill(XL.headerGrey)));
|
||||
|
||||
for (const e of this.otherExpenses) {
|
||||
const row = sheet.addRow([e.srNo, e.particulars, e.amount]);
|
||||
const row = sheet.addRow([
|
||||
e.srNo,
|
||||
e.particulars,
|
||||
e.amount
|
||||
]);
|
||||
row.getCell(3).numFmt = '#,##0.00';
|
||||
}
|
||||
|
||||
const total = sheet.addRow(['Total', '', this.otherExpensesTotal]);
|
||||
const total = sheet.addRow([
|
||||
'Total',
|
||||
'',
|
||||
this.otherExpensesTotal
|
||||
]);
|
||||
sheet.mergeCells(`A${total.number}:B${total.number}`);
|
||||
total.font = { bold: true };
|
||||
total.getCell(1).fill = solidFill(XL.headerGrey);
|
||||
@@ -268,7 +298,11 @@ export class Oss2RoeComponent {
|
||||
|
||||
boxAllCells(sheet, start, sheet.rowCount, 3);
|
||||
|
||||
const ref = sheet.addRow(['', '', 'Refer Earnings Sheet — line 12 ix) Others']);
|
||||
const ref = sheet.addRow([
|
||||
'',
|
||||
'',
|
||||
'Refer Earnings Sheet — line 12 ix) Others'
|
||||
]);
|
||||
ref.getCell(3).font = { italic: true, size: 10 };
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,46 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- GL mapping toolbar (applies to the first figure column of each statement) -->
|
||||
@if (activeTab !== 'filing' && activeTab !== 'signatory') {
|
||||
<div class="oss-gl-bar" [class.on]="glMapMode">
|
||||
<div class="oss-gl-bar-l">
|
||||
<fa-icon icon="project-diagram"></fa-icon>
|
||||
<span>
|
||||
<strong>General Ledger mapping</strong>
|
||||
@if (glMappedCount) {
|
||||
<span class="oss-gl-count">{{ glMappedCount }} line(s) mapped</span>
|
||||
} @else {
|
||||
<span class="muted"> — pin GL accounts to a line and its first figure comes from the ledger</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="oss-gl-bar-r">
|
||||
@if (glMapMode) {
|
||||
@if (canSelectGlBranch) {
|
||||
<select class="oss-gl-inp" [(ngModel)]="glBranchCode">
|
||||
<option value="">All branches</option>
|
||||
@for (b of glBranches; track b.code) {
|
||||
<option [value]="b.code">{{ b.name }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
<span class="oss-gl-scope" title="Your login is restricted to this branch">
|
||||
<fa-icon icon="university"></fa-icon> {{ glScopeName }}
|
||||
</span>
|
||||
}
|
||||
@if (glMappedCount) {
|
||||
<button class="oss-btn oss-btn-clear" (click)="clearAllGlMappings()">Clear all</button>
|
||||
}
|
||||
}
|
||||
<button class="oss-btn" [class.oss-btn-xls]="!glMapMode" (click)="toggleGlMapMode()">
|
||||
<fa-icon [icon]="glMapMode ? 'times' : 'plus'"></fa-icon>
|
||||
{{ glMapMode ? 'Done' : 'Add / Remove GLs' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- FILING INFORMATION -->
|
||||
@if (activeTab === 'filing') {
|
||||
<div class="oss-section">
|
||||
@@ -74,7 +114,9 @@
|
||||
<!-- PORTFOLIO ANALYSIS -->
|
||||
@if (activeTab === 'portfolio') {
|
||||
<div class="oss-section">
|
||||
<div class="oss-sheet-banner">PortfolioAnalysis(loans_advncs) — Portfolio Analysis (By delinquency in Interest Payment)</div>
|
||||
<div class="oss-sheet-banner">
|
||||
PortfolioAnalysis(loans_advncs) — Portfolio Analysis (By delinquency in Interest Payment)
|
||||
</div>
|
||||
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: portfolioAnalysis }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: portfolioMemo }"></ng-container>
|
||||
</div>
|
||||
@@ -117,7 +159,9 @@
|
||||
<ng-container [ngTemplateOutlet]="matrix" [ngTemplateOutletContext]="{ t: capitalMemo }"></ng-container>
|
||||
|
||||
<div class="oss-tbl-wrap">
|
||||
<div class="oss-tbl-h">Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs — Rs in Thousands</div>
|
||||
<div class="oss-tbl-h">
|
||||
Table 5: Top Fifty Impaired Credit or more than Rs.100 lakhs — Rs in Thousands
|
||||
</div>
|
||||
<table class="oss-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -240,7 +284,8 @@
|
||||
Sectoral gross advances <strong>₹{{ sectoralGross | number: '1.2-2' }}</strong> and NPAs
|
||||
<strong>₹{{ sectoralNpa | number: '1.2-2' }}</strong> do not match the closing position reported by
|
||||
Classification (<strong>₹{{ classificationTotal | number: '1.2-2' }}</strong> /
|
||||
<strong>₹{{ grossNpa | number: '1.2-2' }}</strong>).
|
||||
<strong>₹{{ grossNpa | number: '1.2-2' }}</strong
|
||||
>).
|
||||
@if (sectoralMatchesOpening) {
|
||||
They match the <em>opening</em> balances in Movement Parts 1 and 2 instead — this statement appears to
|
||||
have been prepared on the previous quarter's book.
|
||||
@@ -326,7 +371,10 @@
|
||||
<ng-template #matrix let-t="t">
|
||||
<div class="oss-tbl-wrap">
|
||||
<div class="oss-tbl-h">
|
||||
{{ t.title }}@if (t.unit) {<span class="oss-tbl-unit">{{ t.unit }}</span>}
|
||||
{{ t.title }}
|
||||
@if (t.unit) {
|
||||
<span class="oss-tbl-unit">{{ t.unit }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (t.subtitle) {
|
||||
<div class="oss-tbl-sub">{{ t.subtitle }}</div>
|
||||
@@ -335,6 +383,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="oss-col-label">Particulars</th>
|
||||
@if (glMapMode) {
|
||||
<th class="oss-gl-col">Mapped GL accounts</th>
|
||||
}
|
||||
@for (c of t.columns; track $index) {
|
||||
<th class="ta-r">{{ c }}</th>
|
||||
}
|
||||
@@ -344,12 +395,48 @@
|
||||
@for (row of t.rows; track $index) {
|
||||
<tr [class.total-row]="row.total" [class.section-row]="row.header">
|
||||
<td [class]="'lvl-' + row.level">{{ row.label }}</td>
|
||||
@if (glMapMode) {
|
||||
<td>
|
||||
@if (!row.header) {
|
||||
<div class="oss-gl-cell">
|
||||
@for (code of glCodesFor(t.title + ' / ' + row.label); track code) {
|
||||
<span class="oss-gl-chip">
|
||||
<span class="mono">{{ code }}</span>
|
||||
<button
|
||||
class="oss-gl-chip-x"
|
||||
(click)="removeGlFromLine(t.title + ' / ' + row.label, code)"
|
||||
[attr.aria-label]="'Remove ' + code"
|
||||
>
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
<button
|
||||
class="oss-gl-add"
|
||||
(click)="openGlPicker(t.title + ' / ' + row.label, row.values[0] ?? null)"
|
||||
>
|
||||
<fa-icon icon="plus"></fa-icon>
|
||||
{{ glCodesFor(t.title + ' / ' + row.label).length ? 'Edit' : 'Add GL' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (row.header) {
|
||||
<td [attr.colspan]="t.columns.length"></td>
|
||||
} @else {
|
||||
@for (v of row.values; track $index) {
|
||||
<td class="ta-r mono" [class.derived]="row.total">
|
||||
{{ v === null ? '' : (v | number: '1.2-2') }}{{ v !== null && row.percent ? '%' : '' }}
|
||||
<td
|
||||
class="ta-r mono"
|
||||
[class.derived]="row.total"
|
||||
[class.gl-sourced]="$index === 0 && isGlMapped(t.title + ' / ' + row.label)"
|
||||
[class.gl-diff]="$index === 0 && glDiffers(t.title + ' / ' + row.label, v)"
|
||||
>
|
||||
@if ($index === 0 && isGlMapped(t.title + ' / ' + row.label)) {
|
||||
{{ effectiveValue(t.title + ' / ' + row.label, v) | number: '1.2-2' }}{{ row.percent ? '%' : '' }}
|
||||
} @else {
|
||||
{{ v === null ? '' : (v | number: '1.2-2') }}{{ v !== null && row.percent ? '%' : '' }}
|
||||
}
|
||||
</td>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { MatrixTable } from '../oss-return.model';
|
||||
import { GlMappingHost } from '../gl-mapping.host';
|
||||
import {
|
||||
addBanner,
|
||||
addMatrixTable,
|
||||
@@ -77,7 +78,14 @@ type Oss3Tab =
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class Oss3RaqComponent {
|
||||
export class Oss3RaqComponent extends GlMappingHost {
|
||||
readonly glReturnCode = 'R323';
|
||||
|
||||
/** RAQ reports the quarter; mapped figures are drawn from the same span. */
|
||||
get glRange() {
|
||||
return { from: '2026-04-01', to: '2026-06-30' };
|
||||
}
|
||||
|
||||
activeTab: Oss3Tab = 'filing';
|
||||
|
||||
/** Tab strip definition — keeps the template free of a dozen near-identical buttons. */
|
||||
@@ -313,8 +321,7 @@ export class Oss3RaqComponent {
|
||||
{ width: 16 },
|
||||
{ width: 18 },
|
||||
{ width: 18 },
|
||||
{ width: 18 }
|
||||
];
|
||||
{ width: 18 }];
|
||||
addBanner(sheet, 'TopImpairedCredits - Top Impaired Credits', 'I');
|
||||
addMatrixTable(sheet, this.capitalMemo);
|
||||
for (const t of this.recoveryTables) {
|
||||
@@ -398,8 +405,7 @@ export class Oss3RaqComponent {
|
||||
{ width: 18 },
|
||||
{ width: 20 },
|
||||
{ width: 22 },
|
||||
{ width: 20 }
|
||||
];
|
||||
{ width: 20 }];
|
||||
addBanner(sheet, 'ExposureLimits - Single and Group Borrower Exposure Limit', 'G');
|
||||
addMatrixTable(sheet, this.capitalPreviousMarch);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user