486 lines
19 KiB
HTML
486 lines
19 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="tdsr">
|
||
<div class="tdsr__head">
|
||
<div class="tdsr__head-l">
|
||
<fa-icon icon="percentage" class="tdsr__head-ic"></fa-icon>
|
||
<div>
|
||
<h2 class="tdsr__title">Deposit Interest & TDS</h2>
|
||
<div class="tdsr__sub">
|
||
Interest each customer earned this year, and whether TDS has to be deducted on it ·
|
||
<strong>Showing: {{ scopeLabel }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="tdsr__head-r">
|
||
<button type="button" class="tdsr__btn" (click)="showHelp = !showHelp">
|
||
<fa-icon icon="question-circle"></fa-icon>
|
||
{{ showHelp ? 'Hide help' : 'How to read this page' }}
|
||
</button>
|
||
<button type="button" class="tdsr__btn" (click)="exportWorkbook()" [disabled]="exporting">
|
||
<fa-icon icon="download"></fa-icon>
|
||
{{ exporting ? 'Preparing…' : 'Export' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
@if (loadError) {
|
||
<div class="tdsr__banner tdsr__banner--warn">
|
||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||
<div>{{ loadError }}</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Help -->
|
||
@if (showHelp) {
|
||
<div class="tdsr__help">
|
||
<div class="tdsr__help-title"><fa-icon icon="info-circle"></fa-icon> How to read this page</div>
|
||
<ol class="tdsr__help-list">
|
||
<li>
|
||
Every customer with an account is listed. Each account is either
|
||
<span class="tdsr__pill tdsr__pill--ok">Tracked</span> — its interest counts toward the TDS limit — or
|
||
<span class="tdsr__pill">Not tracked</span>. Open a row (<fa-icon icon="chevron-down"></fa-icon>) to see which
|
||
account is which, and why.
|
||
</li>
|
||
<li>
|
||
Only <strong>fixed and recurring deposits</strong> are tracked. Savings interest is shown for information but
|
||
never attracts TDS.
|
||
</li>
|
||
@if (rules) {
|
||
<li>
|
||
A customer's tracked interest for the whole year, across every branch, is checked against the limit:
|
||
<strong>₹{{ rules.general | number: '1.0-0' }}</strong
|
||
>, or <strong>₹{{ rules.senior | number: '1.0-0' }}</strong> for a senior citizen (60+).
|
||
</li>
|
||
<li>
|
||
Once over the limit, TDS is <strong>{{ rules.rate | number: '1.0-2' }}%</strong> of the
|
||
<em>whole</em> year's interest — <strong>{{ rules.rateNoPan | number: '1.0-2' }}%</strong> if no PAN is on
|
||
record.
|
||
</li>
|
||
}
|
||
<li>A customer who has submitted Form 15G/15H is not deducted, even over the limit.</li>
|
||
<li>
|
||
The TDS figures here are <strong>worked out by this report</strong> from the interest actually posted. They
|
||
are not debited from the customer's account automatically.
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
}
|
||
|
||
<!-- Summary -->
|
||
<div class="tdsr__kpis">
|
||
<div class="tdsr__kpi">
|
||
<div class="tdsr__kpi-n">{{ summary.deductees | number }}</div>
|
||
<div class="tdsr__kpi-l">Customers · {{ summary.trackedCustomers | number }} with an FD/RD being tracked</div>
|
||
</div>
|
||
<div class="tdsr__kpi">
|
||
<div class="tdsr__kpi-n">₹{{ summary.applicableInterest | number: '1.0-0' }}</div>
|
||
<div class="tdsr__kpi-l">FD / RD interest — counts toward the limit</div>
|
||
</div>
|
||
<div class="tdsr__kpi tdsr__kpi--muted">
|
||
<div class="tdsr__kpi-n">₹{{ summary.exemptInterest | number: '1.0-0' }}</div>
|
||
<div class="tdsr__kpi-l">Savings interest — shown only, no TDS</div>
|
||
</div>
|
||
<div class="tdsr__kpi tdsr__kpi--warn">
|
||
<div class="tdsr__kpi-n">{{ summary.crossed | number }}</div>
|
||
<div class="tdsr__kpi-l">Customers over the limit</div>
|
||
</div>
|
||
<div class="tdsr__kpi tdsr__kpi--warn">
|
||
<div class="tdsr__kpi-n">₹{{ summary.totalTds | number: '1.0-0' }}</div>
|
||
<div class="tdsr__kpi-l">TDS to be deducted</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Things worth acting on, only when there are any -->
|
||
@if (summary.nearThreshold || summary.declarations || summary.noPan) {
|
||
<div class="tdsr__chips">
|
||
@if (summary.nearThreshold) {
|
||
<button type="button" class="tdsr__chip tdsr__chip--near" (click)="showNearThreshold()">
|
||
{{ summary.nearThreshold | number }} near the limit (within ₹{{ nearBand | number: '1.0-0' }}) — ask for
|
||
15G/15H
|
||
</button>
|
||
}
|
||
@if (summary.declarations) {
|
||
<span class="tdsr__chip tdsr__chip--ok">
|
||
{{ summary.declarations | number }} with 15G/15H — ₹{{ summary.tdsSaved | number: '1.0-0' }} not deducted
|
||
</span>
|
||
}
|
||
@if (summary.noPan) {
|
||
<span class="tdsr__chip tdsr__chip--warn">
|
||
{{ summary.noPan | number }} FD/RD customers without PAN — higher rate applies
|
||
</span>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<!-- Filters -->
|
||
<div class="tdsr__filters">
|
||
<label class="tdsr__filter">
|
||
<span>Financial year</span>
|
||
<select [(ngModel)]="filter.fy" (ngModelChange)="onYearChanged()">
|
||
@for (year of financialYears; track year) {
|
||
<option [value]="year">FY {{ year }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
|
||
@if (canSelectBranch) {
|
||
<label class="tdsr__filter">
|
||
<span>Branch</span>
|
||
<select [(ngModel)]="filter.branchCode" (ngModelChange)="onFilterChanged()">
|
||
<option value="">All branches</option>
|
||
@for (b of branches; track b) {
|
||
<option [value]="b">{{ b }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
}
|
||
|
||
<label class="tdsr__filter">
|
||
<span>Quarter</span>
|
||
<select [(ngModel)]="filter.quarter" (ngModelChange)="onFilterChanged()">
|
||
<option [ngValue]="0">Whole year</option>
|
||
@for (q of quarters; track q) {
|
||
<option [ngValue]="q">{{ quarterLabel[q] }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
|
||
<label class="tdsr__filter">
|
||
<span>Age group</span>
|
||
<select [(ngModel)]="filter.band" (ngModelChange)="onFilterChanged()">
|
||
<option value="">Everyone</option>
|
||
<option value="GENERAL">Below 60</option>
|
||
<option value="SENIOR">Senior citizen (60+)</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="tdsr__filter tdsr__filter--wide">
|
||
<span>Search customer</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="filter.search"
|
||
(ngModelChange)="onFilterChanged()"
|
||
placeholder="Name, customer ID or PAN"
|
||
/>
|
||
</label>
|
||
|
||
<label class="tdsr__check">
|
||
<input type="checkbox" [(ngModel)]="filter.crossedOnly" (ngModelChange)="onFilterChanged()" />
|
||
<span>Over the limit only</span>
|
||
</label>
|
||
|
||
<label class="tdsr__check">
|
||
<input type="checkbox" [(ngModel)]="filter.nearThresholdOnly" (ngModelChange)="onFilterChanged()" />
|
||
<span>Near the limit only</span>
|
||
</label>
|
||
|
||
@if (hasFilters) {
|
||
<button type="button" class="tdsr__clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||
}
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="tdsr__tabs">
|
||
<button
|
||
type="button"
|
||
class="tdsr__tab"
|
||
[class.tdsr__tab--active]="selectedTab === 'paying'"
|
||
(click)="selectTab('paying')"
|
||
>
|
||
TDS to deduct ({{ payingRows.length }})
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="tdsr__tab"
|
||
[class.tdsr__tab--active]="selectedTab === 'not-paying'"
|
||
(click)="selectTab('not-paying')"
|
||
>
|
||
No TDS ({{ notPayingRows.length }})
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<div class="tdsr__card">
|
||
<div class="tdsr__table-wrap">
|
||
<table class="tdsr__table">
|
||
<thead>
|
||
<tr>
|
||
<th>Customer</th>
|
||
<th>Branch</th>
|
||
<th>Accounts</th>
|
||
<th class="num">FD / RD interest this year</th>
|
||
<th class="num">Limit</th>
|
||
<th>Status</th>
|
||
<th class="num">TDS</th>
|
||
<th class="col-actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (row of pagedRows; track trackRow($index, row)) {
|
||
<tr class="tdsr__row--click" [class.tdsr__row--near]="row.nearThreshold" (click)="toggleExpanded(row)">
|
||
<td>
|
||
<div class="tdsr__name">{{ row.deductee.name }}</div>
|
||
<div class="mono small muted">
|
||
ID {{ row.deductee.customerId }} ·
|
||
@if (row.panOnRecord) {
|
||
PAN {{ row.deductee.pan }}
|
||
} @else {
|
||
<span class="tdsr__pill tdsr__pill--warn">no PAN</span>
|
||
}
|
||
@if (row.band === 'SENIOR') {
|
||
<span class="tdsr__pill">Senior 60+</span>
|
||
}
|
||
</div>
|
||
</td>
|
||
<td class="small">{{ row.branchCode || '—' }}</td>
|
||
<td class="small">
|
||
@if (row.trackedAccounts) {
|
||
<span class="tdsr__pill tdsr__pill--ok">{{ row.trackedAccounts }} tracked</span>
|
||
}
|
||
@if (untrackedOf(row).length) {
|
||
<span class="tdsr__pill">{{ untrackedOf(row).length }} not tracked</span>
|
||
}
|
||
</td>
|
||
<td class="num">
|
||
<strong>₹{{ row.tdsApplicableInterest | number: '1.0-0' }}</strong>
|
||
@if (row.exemptInterest) {
|
||
<div class="small muted">+ ₹{{ row.exemptInterest | number: '1.0-0' }} savings (no TDS)</div>
|
||
}
|
||
</td>
|
||
<td class="num small muted">₹{{ row.threshold | number: '1.0-0' }}</td>
|
||
<td class="small">
|
||
<span
|
||
class="tdsr__pill"
|
||
[class.tdsr__pill--warn]="statusOf(row).tone === 'warn'"
|
||
[class.tdsr__pill--near]="statusOf(row).tone === 'near'"
|
||
[class.tdsr__pill--ok]="statusOf(row).tone === 'ok'"
|
||
>
|
||
{{ statusOf(row).label }}
|
||
</span>
|
||
</td>
|
||
<td class="num">
|
||
@if (row.schedule.totalTds) {
|
||
<strong>₹{{ row.schedule.totalTds | number: '1.0-0' }}</strong>
|
||
<div class="small muted">@ {{ row.rateApplied * 100 | number: '1.0-0' }}%</div>
|
||
} @else if (row.tdsSaved) {
|
||
<span class="small tdsr__ok">₹{{ row.tdsSaved | number: '1.0-0' }} saved</span>
|
||
} @else {
|
||
<span class="muted">—</span>
|
||
}
|
||
</td>
|
||
<td class="col-actions">
|
||
<button
|
||
type="button"
|
||
class="tdsr__icon-btn"
|
||
[attr.aria-label]="isExpanded(row) ? 'Hide details' : 'Show details'"
|
||
(click)="$event.stopPropagation(); toggleExpanded(row)"
|
||
>
|
||
<fa-icon [icon]="isExpanded(row) ? 'chevron-up' : 'chevron-down'"></fa-icon>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
|
||
@if (isExpanded(row)) {
|
||
<tr class="tdsr__row--detail">
|
||
<td colspan="8">
|
||
<!-- 1. Which accounts are tracked -->
|
||
<div class="tdsr__detail-h">Accounts of {{ row.deductee.name }}</div>
|
||
<table class="tdsr__table tdsr__table--inner">
|
||
<thead>
|
||
<tr>
|
||
<th>Account no.</th>
|
||
<th>Type</th>
|
||
<th>Tracked for TDS?</th>
|
||
<th class="num">Interest this year</th>
|
||
<th class="num">Postings</th>
|
||
<th>Last posted</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (a of row.accounts; track a.accountNo) {
|
||
<tr>
|
||
<td class="mono small">{{ a.accountNo }}</td>
|
||
<td class="small">{{ accountTypeLabel[a.type] }}</td>
|
||
<td class="small">
|
||
@if (a.tracked) {
|
||
<span class="tdsr__pill tdsr__pill--ok"><fa-icon icon="check"></fa-icon> Yes</span>
|
||
} @else {
|
||
<span class="tdsr__pill">No</span>
|
||
}
|
||
<span class="muted"> {{ a.reason }}</span>
|
||
</td>
|
||
<td class="num">₹{{ a.interest | number: '1.2-2' }}</td>
|
||
<td class="num small">{{ a.postings }}</td>
|
||
<td class="small muted">
|
||
{{ a.lastPostedOn ? (a.lastPostedOn | date: 'd MMM y') : 'No interest posted yet' }}
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 2. How the TDS was worked out -->
|
||
@if (row.trackedAccounts) {
|
||
<div class="tdsr__detail-h">How the TDS is worked out, quarter by quarter</div>
|
||
@for (note of row.schedule.notes; track note) {
|
||
<div class="tdsr__note">
|
||
<fa-icon icon="info-circle"></fa-icon>
|
||
<div>{{ note }}</div>
|
||
</div>
|
||
}
|
||
@if (row.schedule.lines.length) {
|
||
<table class="tdsr__table tdsr__table--inner">
|
||
<thead>
|
||
<tr>
|
||
<th>Quarter</th>
|
||
<th class="num">Interest in quarter</th>
|
||
<th class="num">Total so far</th>
|
||
<th class="num">TDS this quarter</th>
|
||
<th>Return due</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (line of row.schedule.lines; track line.quarter) {
|
||
<tr [class.tdsr__row--warn]="line.thresholdCrossedHere">
|
||
<td class="small">
|
||
{{ line.label }}
|
||
@if (line.thresholdCrossedHere) {
|
||
<span class="tdsr__pill tdsr__pill--warn">limit crossed here</span>
|
||
}
|
||
@if (line.declarationActive) {
|
||
<span class="tdsr__pill tdsr__pill--ok">15G/15H active</span>
|
||
}
|
||
</td>
|
||
<td class="num">₹{{ line.interest | number: '1.0-0' }}</td>
|
||
<td class="num">
|
||
₹{{ line.cumInterest | number: '1.0-0' }}
|
||
<span class="small muted">of ₹{{ row.threshold | number: '1.0-0' }}</span>
|
||
</td>
|
||
<td class="num">
|
||
@if (line.tds !== 0) {
|
||
<strong>₹{{ line.tds | number: '1.0-0' }}</strong>
|
||
} @else {
|
||
<span class="muted">—</span>
|
||
}
|
||
</td>
|
||
<td class="small muted">{{ line.dueDate }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} @else {
|
||
<div class="small muted">No interest has been posted on the tracked deposits this year yet.</div>
|
||
}
|
||
@if (row.paidWithoutDeduction) {
|
||
<div class="tdsr__meta">
|
||
<span>
|
||
Paid without deduction because of 15G/15H:
|
||
<strong>₹{{ row.paidWithoutDeduction | number: '1.0-0' }}</strong> — still reported in
|
||
{{ statute.statement }}
|
||
</span>
|
||
</div>
|
||
}
|
||
} @else {
|
||
<div class="tdsr__note">
|
||
<fa-icon icon="info-circle"></fa-icon>
|
||
<div>
|
||
This customer has no fixed or recurring deposit, so nothing here counts toward TDS. Savings
|
||
interest is shown for information only.
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (row.jointHeldInterest) {
|
||
<div class="tdsr__note">
|
||
<fa-icon icon="info-circle"></fa-icon>
|
||
<div>
|
||
₹{{ row.jointHeldInterest | number: '1.0-0' }} of interest is on joint deposits where this
|
||
customer is not the first-named holder, so it is counted under that holder instead.
|
||
</div>
|
||
</div>
|
||
}
|
||
</td>
|
||
</tr>
|
||
}
|
||
}
|
||
@if (visibleRows.length === 0) {
|
||
<tr>
|
||
<td colspan="8" class="tdsr__empty">
|
||
<fa-icon icon="info-circle"></fa-icon>
|
||
@if (loading) {
|
||
Loading…
|
||
} @else if (selectedTab === 'paying') {
|
||
No customer needs TDS deducted for these filters. Check the "No TDS" tab.
|
||
} @else {
|
||
No customers match these filters.
|
||
}
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
@if (visibleRows.length > 0) {
|
||
<div class="tdsr__pager">
|
||
<div class="tdsr__pager-l">
|
||
<span>
|
||
Showing <strong>{{ rangeStart | number }}–{{ rangeEnd | number }}</strong> of
|
||
<strong>{{ visibleRows.length | number }}</strong> customers
|
||
</span>
|
||
<label class="tdsr__pager-sizel">
|
||
Rows
|
||
<select class="tdsr__pager-size" [ngModel]="pageSize" (ngModelChange)="onPageSizeChange($event)">
|
||
@for (size of pageSizeOptions; track size) {
|
||
<option [ngValue]="size">{{ size || 'All' }}</option>
|
||
}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
@if (pageCount > 1) {
|
||
<div class="tdsr__pager-r">
|
||
<button type="button" class="tdsr__pager-btn" (click)="goToPage(0)" [disabled]="currentPage === 0">
|
||
First
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="tdsr__pager-btn"
|
||
(click)="goToPage(currentPage - 1)"
|
||
[disabled]="currentPage === 0"
|
||
aria-label="Previous page"
|
||
>
|
||
<fa-icon icon="chevron-left"></fa-icon>
|
||
</button>
|
||
<span class="tdsr__pager-cur">Page {{ currentPage + 1 | number }} of {{ pageCount | number }}</span>
|
||
<button
|
||
type="button"
|
||
class="tdsr__pager-btn"
|
||
(click)="goToPage(currentPage + 1)"
|
||
[disabled]="currentPage >= pageCount - 1"
|
||
aria-label="Next page"
|
||
>
|
||
<fa-icon icon="chevron-right"></fa-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="tdsr__pager-btn"
|
||
(click)="goToPage(pageCount - 1)"
|
||
[disabled]="currentPage >= pageCount - 1"
|
||
>
|
||
Last
|
||
</button>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|