Files
cbsnexora/src/app/organization/rbi-reserve-config/rbi-reserve-config.component.html
T

337 lines
14 KiB
HTML

<!--
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="rrc-container">
<div class="rrc-header">
<button mat-stroked-button [routerLink]="['/organization']" class="rrc-back">
<i class="fas fa-arrow-left"></i> Organization
</button>
<h2 class="rrc-header__title">{{ screenTitle }}</h2>
</div>
<p class="rrc-sub">{{ screenSubtitle }}</p>
@if (errorMsg) {
<div class="rrc-alert rrc-alert--error">{{ errorMsg }}</div>
}
@if (successMsg) {
<div class="rrc-alert rrc-alert--success">{{ successMsg }}</div>
}
@if (loading) {
<div class="rrc-loading">Loading…</div>
} @else {
<!-- ═══ Rates — only the reserve returns are charged at a percentage ═══ -->
@if (showRates) {
<div class="rrc-card">
<h3 class="rrc-card__title">Reserve rates</h3>
<p class="rrc-card__hint">
Rates are effective-dated rather than overwritten: a return filed for an earlier period keeps reporting the rate
that was actually in force then. Saving against a date that already has a rate edits it; a new date adds one.
</p>
<div class="rrc-rate-now">
@for (type of ['CRR', 'SLR', 'SAVINGS_DEMAND']; track type) {
<div class="rrc-rate-now__item">
<span class="rrc-rate-now__label">
{{ type === 'SAVINGS_DEMAND' ? 'Savings demand share' : type }} in force today
</span>
@if (currentRate(type); as rate) {
<span class="rrc-rate-now__value">{{ rate.percentage }}%</span>
@if (rate.seedDefault) {
<span class="rrc-rate-now__since rrc-rate-now__since--warn">
Placeholder — confirm the rate actually in force
</span>
} @else {
<span class="rrc-rate-now__since">since {{ rate.effectiveFrom }}</span>
}
} @else {
<span class="rrc-rate-now__value rrc-rate-now__value--none">Not set</span>
<span class="rrc-rate-now__since">{{ type === 'CRR' ? 'Form 1' : 'Form 9' }} cannot compute</span>
}
</div>
}
</div>
<div class="rrc-rate-form">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__type">
<mat-label>Rate</mat-label>
<mat-select [(ngModel)]="newRate.rateType">
<mat-option value="CRR">CRR — Form 1</mat-option>
<mat-option value="SLR">SLR — Form 9</mat-option>
<!-- Not a reserve ratio, but set by RBI and effective-dated the same way: the share of
savings balances that counts as a demand liability, the rest being time. -->
<mat-option value="SAVINGS_DEMAND">Savings — demand share</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__pct">
<mat-label>Percentage (%)</mat-label>
<input matInput type="number" step="0.0001" min="0" max="100" [(ngModel)]="newRate.percentage" />
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-rate-form__date">
<mat-label>Effective from</mat-label>
<input matInput type="date" [(ngModel)]="newRate.effectiveFrom" />
</mat-form-field>
<button mat-raised-button color="primary" [disabled]="savingRate" (click)="saveRate()">
{{ savingRate ? 'Saving…' : 'Save rate' }}
</button>
</div>
<table class="rrc-table">
<thead>
<tr>
<th>Rate</th>
<th>Percentage</th>
<th>Effective from</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@for (rate of rates; track rate.id) {
<tr>
<td><span class="rrc-key">{{ rate.rateType }}</span></td>
<td class="rrc-num">{{ rate.percentage }}%</td>
<td>{{ rate.effectiveFrom }}</td>
<td>
@if (isFuture(rate)) {
<span class="rrc-badge rrc-badge--future">Future-dated</span>
} @else if (currentRate(rate.rateType)?.id === rate.id) {
<span class="rrc-badge rrc-badge--active">In force</span>
} @else {
<span class="rrc-badge rrc-badge--past">Superseded</span>
}
@if (rate.seedDefault) {
<span class="rrc-badge rrc-badge--seed">Placeholder</span>
}
</td>
<td>
<button mat-button class="rrc-remove" (click)="deleteRate(rate)" title="Remove this rate">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
<!-- ═══ GL accounts ═══ -->
<div class="rrc-card">
<h3 class="rrc-card__title">GL accounts behind each figure</h3>
<p class="rrc-card__hint">
Every role takes as many accounts as it needs — a bank holds current accounts at several other banks, and each
one counts toward what it maintains. Only postable accounts are listed: a header account's balance is the sum of
its children, so mapping both would count those children twice.
</p>
@if (unmappedCount) {
<div class="rrc-alert rrc-alert--warn">
{{ unmappedCount }} of {{ glRoles.length }} roles have no accounts mapped. Returns that depend on them will
report why they cannot compute instead of showing a figure.
</div>
}
<!--
One tab per return. The roles of four returns on a single list would bury the four that
drive CRR among the thirty that do not, so each return's roles are configured on their own.
Each tab carries its own unmapped count, so a return that cannot compute is visible without
opening its tab.
-->
@if (showTabs) {
<div class="rrc-tabs" role="tablist">
@for (group of roleGroups; track group.name) {
<button
type="button"
role="tab"
class="rrc-tab"
[class.rrc-tab--active]="group.name === activeGroup"
[attr.aria-selected]="group.name === activeGroup"
(click)="showGroup(group.name)"
>
{{ group.label }}
@if (unmappedIn(group)) {
<span class="rrc-tab__badge">{{ unmappedIn(group) }} unmapped</span>
} @else {
<span class="rrc-tab__badge rrc-tab__badge--done">all mapped</span>
}
</button>
}
</div>
}
@for (role of visibleRoles; track role.configKey) {
<div class="rrc-role" [class.rrc-role--unmapped]="isUnmapped(role)">
<div class="rrc-role__head">
<div>
<span class="rrc-role__name">{{ roleLabel(role) }}</span>
<span class="rrc-key">{{ role.configKey }}</span>
<span class="rrc-role__usedby">{{ roleUsedBy(role) }}</span>
</div>
@if (isUnmapped(role)) {
<span class="rrc-badge rrc-badge--unmapped">Not mapped</span>
} @else {
<span class="rrc-badge rrc-badge--mapped">
{{ selection[role.configKey].length }} account{{ selection[role.configKey].length === 1 ? '' : 's' }}
</span>
}
</div>
<p class="rrc-role__desc">{{ role.description }}</p>
<!--
One row per mapping rather than a multi-select, because a mapping is no longer just an
account: it carries the portion of the balance this head takes, and which head that
portion falls under. A savings account appears twice here - a demand slice and a time
slice - which a multi-select cannot express.
-->
@if (selection[role.configKey]?.length) {
<table class="rrc-map">
<thead>
<tr>
<th>GL account</th>
<th class="rrc-map__narrow">Portion (%)</th>
@if (role.classified) {
<th class="rrc-map__narrow">Head</th>
}
<th class="rrc-map__actions"></th>
</tr>
</thead>
<tbody>
@for (mapping of selection[role.configKey]; track $index; let i = $index) {
<tr [class.rrc-map__row--invalid]="isRowInvalid(mapping)">
<td>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-map__field">
<mat-select [(ngModel)]="mapping.glAccountId" [disabled]="savingKey === role.configKey">
@for (o of availableAccounts(role, i); track o.id) {
<mat-option [value]="o.id">{{ o.glCode }} · {{ o.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</td>
<td>
<!-- The share of this account's balance the head takes. 100 is the whole of it,
which is what every mapping meant before splits existed. -->
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-map__field">
<input
matInput
type="number"
min="0"
max="100"
step="0.0001"
[(ngModel)]="mapping.weightPercentage"
[disabled]="savingKey === role.configKey"
/>
</mat-form-field>
</td>
@if (role.classified) {
<td>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-map__field">
<mat-select [(ngModel)]="mapping.liabilityClass" [disabled]="savingKey === role.configKey">
@for (c of liabilityClasses; track c.value) {
<mat-option [value]="c.value">{{ c.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</td>
}
<td class="rrc-map__actions">
<button mat-button class="rrc-remove" (click)="removeMapping(role, i)" title="Remove this row">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
}
<!--
An account split across rows whose portions do not add to the whole balance is the one
error a split invites and no single row shows: each row looks reasonable while a share of
the balance silently leaves the base.
-->
@for (entry of unbalancedAccounts(role); track entry.glAccountId) {
<div class="rrc-alert rrc-alert--warn rrc-alert--inline">
{{ glLabel(entry.glAccountId) }} is mapped at {{ entry.total }}% in total.
@if (entry.total < 100) {
{{ 100 - entry.total }}% of its balance will not appear on the return.
} @else {
It would be counted {{ entry.total }}% — more than the account holds.
}
</div>
}
<div class="rrc-role__controls">
<button mat-stroked-button (click)="addMapping(role)" [disabled]="savingKey === role.configKey">
<i class="fas fa-plus"></i> Add account
</button>
<button
mat-raised-button
color="primary"
[disabled]="savingKey === role.configKey || !isDirty(role) || hasInvalidRows(role)"
(click)="saveRole(role)"
>
{{ savingKey === role.configKey ? 'Saving…' : 'Save' }}
</button>
@if (!role.builtIn) {
<button mat-button class="rrc-remove" (click)="deleteRole(role)" title="Remove this head">
<i class="fas fa-trash"></i> Remove head
</button>
}
</div>
</div>
}
<!-- ═══ add a head ═══ -->
<div class="rrc-add">
@if (!showAddRole) {
<button mat-stroked-button (click)="showAddRole = true">
<i class="fas fa-plus"></i> Add a head to this return
</button>
<p class="rrc-card__hint">
For anything this bank carries that the heads above do not name — a reserve, a tax payable, bills payable.
The return reads it from the same chart of accounts the balance sheet is built from.
</p>
} @else {
<div class="rrc-add__form">
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>Name</mat-label>
<input matInput [(ngModel)]="newRole.label" placeholder="e.g. Reserve Fund" />
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>How the return uses it</mat-label>
<mat-select [(ngModel)]="newRole.usage">
@for (u of roleUsages; track u.value) {
<mat-option [value]="u.value">{{ u.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="rrc-add__desc">
<mat-label>Note (optional)</mat-label>
<input matInput [(ngModel)]="newRole.description" placeholder="What this head covers" />
</mat-form-field>
<button mat-raised-button color="primary" [disabled]="addingRole" (click)="addRole()">
{{ addingRole ? 'Adding…' : 'Add head' }}
</button>
<button mat-button (click)="showAddRole = false">Cancel</button>
</div>
}
</div>
</div>
}
</div>