406 lines
16 KiB
HTML
406 lines
16 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">
|
||
Quarter-wise and yearly interest on time deposits, with tax deductible under {{ statute.section }} ·
|
||
{{ scopeLabel }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="tdsr__btn" (click)="exportWorkbook()" [disabled]="exporting">
|
||
<fa-icon icon="download"></fa-icon>
|
||
{{ exporting ? 'Preparing…' : 'Export' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Summary -->
|
||
<div class="tdsr__kpis">
|
||
<div class="tdsr__kpi">
|
||
<div class="tdsr__kpi-n">{{ summary.deductees | number }}</div>
|
||
<div class="tdsr__kpi-l">Deductees</div>
|
||
</div>
|
||
<div class="tdsr__kpi">
|
||
<div class="tdsr__kpi-n">₹{{ summary.applicableInterest | number: '1.0-0' }}</div>
|
||
<div class="tdsr__kpi-l">Interest on FD / RD</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 — outside the charge</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 payable</div>
|
||
</div>
|
||
<div class="tdsr__kpi tdsr__kpi--warn">
|
||
<div class="tdsr__kpi-n">{{ summary.crossed | number }}</div>
|
||
<div class="tdsr__kpi-l">Crossed the threshold</div>
|
||
</div>
|
||
<button type="button" class="tdsr__kpi tdsr__kpi--action" (click)="showNearThreshold()">
|
||
<div class="tdsr__kpi-n">{{ summary.nearThreshold | number }}</div>
|
||
<div class="tdsr__kpi-l">Within ₹{{ nearBand | number: '1.0-0' }} of it — chase a declaration</div>
|
||
</button>
|
||
<div class="tdsr__kpi tdsr__kpi--ok">
|
||
<div class="tdsr__kpi-n">₹{{ summary.tdsSaved | number: '1.0-0' }}</div>
|
||
<div class="tdsr__kpi-l">Saved by {{ summary.declarations | number }} declarations</div>
|
||
</div>
|
||
@if (summary.noPan > 0) {
|
||
<div class="tdsr__kpi tdsr__kpi--warn">
|
||
<div class="tdsr__kpi-n">{{ summary.noPan | number }}</div>
|
||
<div class="tdsr__kpi-l">No PAN — deducted at 20%</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Quarter strip -->
|
||
<div class="tdsr__card">
|
||
<div class="tdsr__card-head"><fa-icon icon="calendar"></fa-icon> Quarter by quarter</div>
|
||
<div class="tdsr__qstrip">
|
||
@for (q of summary.byQuarter; track q.quarter) {
|
||
<div class="tdsr__qcell">
|
||
<div class="tdsr__qcell-l">{{ quarterLabel[q.quarter] }}</div>
|
||
<div class="tdsr__qcell-n">₹{{ q.interest | number: '1.0-0' }}</div>
|
||
<div class="tdsr__qcell-s">TDS ₹{{ q.tds | number: '1.0-0' }}</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="tdsr__filters">
|
||
<label class="tdsr__filter">
|
||
<span>Financial year</span>
|
||
<select [(ngModel)]="filter.fy" (ngModelChange)="onFilterChanged()">
|
||
@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.code) {
|
||
<option [value]="b.code">{{ b.name }}</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>Threshold band</span>
|
||
<select [(ngModel)]="filter.band" (ngModelChange)="onFilterChanged()">
|
||
<option value="">Both bands</option>
|
||
<option value="GENERAL">General (below 60)</option>
|
||
<option value="SENIOR">Senior citizen (60+)</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="tdsr__filter tdsr__filter--wide">
|
||
<span>Customer / CIF / PAN</span>
|
||
<input
|
||
type="text"
|
||
[(ngModel)]="filter.search"
|
||
(ngModelChange)="onFilterChanged()"
|
||
placeholder="name, CIF or PAN"
|
||
/>
|
||
</label>
|
||
|
||
<label class="tdsr__check">
|
||
<input type="checkbox" [(ngModel)]="filter.crossedOnly" (ngModelChange)="onFilterChanged()" />
|
||
<span>Threshold crossed only</span>
|
||
</label>
|
||
|
||
<label class="tdsr__check">
|
||
<input type="checkbox" [(ngModel)]="filter.nearThresholdOnly" (ngModelChange)="onFilterChanged()" />
|
||
<span>Near threshold only</span>
|
||
</label>
|
||
|
||
@if (hasFilters) {
|
||
<button type="button" class="tdsr__clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
|
||
}
|
||
</div>
|
||
|
||
<!-- Tabs Selector -->
|
||
<div class="tdsr__tabs">
|
||
<button
|
||
type="button"
|
||
class="tdsr__tab"
|
||
[class.tdsr__tab--active]="selectedTab === 'paying'"
|
||
(click)="selectTab('paying')"
|
||
>
|
||
Paying TDS ({{ payingRows.length }})
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="tdsr__tab"
|
||
[class.tdsr__tab--active]="selectedTab === 'not-paying'"
|
||
(click)="selectTab('not-paying')"
|
||
>
|
||
Not Paying TDS ({{ notPayingRows.length }})
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<div class="tdsr__card">
|
||
<div class="tdsr__table-wrap">
|
||
<table class="tdsr__table">
|
||
<thead>
|
||
<tr>
|
||
<th>Customer · PAN</th>
|
||
<th>Branch</th>
|
||
<th>Form · Band</th>
|
||
<th class="num">Q1</th>
|
||
<th class="num">Q2</th>
|
||
<th class="num">Q3</th>
|
||
<th class="num">Q4</th>
|
||
<th class="num">Year total</th>
|
||
<th class="num">Threshold</th>
|
||
<th>Crossed</th>
|
||
<th class="num">TDS</th>
|
||
<th class="num">Saved</th>
|
||
<th class="col-actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (row of pagedRows; track trackRow($index, row)) {
|
||
<tr [class.tdsr__row--warn]="!row.panOnRecord" [class.tdsr__row--near]="row.nearThreshold">
|
||
<td>
|
||
<div class="tdsr__name">{{ row.deductee.name }}</div>
|
||
<div class="mono small muted">
|
||
{{ row.deductee.customerId }} ·
|
||
@if (row.panOnRecord) {
|
||
{{ row.deductee.pan }}
|
||
} @else {
|
||
<span class="tdsr__pill tdsr__pill--warn">no PAN</span>
|
||
}
|
||
</div>
|
||
</td>
|
||
<td class="small">{{ branchName(row.branchCode) }}</td>
|
||
<td>
|
||
<span class="tdsr__pill" [class.tdsr__pill--ok]="row.hasDeclaration">
|
||
{{ row.form === '121' ? '121' : row.form }}
|
||
</span>
|
||
<div class="small muted">{{ row.band === 'SENIOR' ? 'Senior' : 'General' }}</div>
|
||
@if (row.hasDeclaration) {
|
||
<div class="mono small muted">{{ row.declarationUin }}</div>
|
||
}
|
||
</td>
|
||
@for (q of quarters; track q) {
|
||
<td class="num small">
|
||
@if (row.quarters[q]) {
|
||
₹{{ row.quarters[q] | number: '1.0-0' }}
|
||
} @else {
|
||
<span class="muted">—</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' }} SB (exempt)</div>
|
||
}
|
||
</td>
|
||
<td class="num small muted">₹{{ row.threshold | number: '1.0-0' }}</td>
|
||
<td class="small">
|
||
@if (row.crossedInQuarter) {
|
||
<span class="tdsr__pill tdsr__pill--warn">Q{{ row.crossedInQuarter }}</span>
|
||
} @else if (row.nearThreshold) {
|
||
<span class="tdsr__pill tdsr__pill--near"> ₹{{ row.headroom | number: '1.0-0' }} left </span>
|
||
} @else {
|
||
<span class="muted">—</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 {
|
||
<span class="muted">—</span>
|
||
}
|
||
</td>
|
||
<td class="num">
|
||
@if (row.tdsSaved) {
|
||
<strong class="tdsr__ok">₹{{ row.tdsSaved | number: '1.0-0' }}</strong>
|
||
} @else {
|
||
<span class="muted">—</span>
|
||
}
|
||
</td>
|
||
<td class="col-actions">
|
||
<button type="button" class="tdsr__icon-btn" (click)="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="13">
|
||
@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.jointHeldInterest) {
|
||
<div class="tdsr__note">
|
||
<fa-icon icon="info-circle"></fa-icon>
|
||
<div>
|
||
₹{{ row.jointHeldInterest | number: '1.0-0' }} of interest sits on deposits where this customer
|
||
is a joint holder but not the first-named one. Tax there is deducted against the first-named
|
||
holder, so it is excluded from the figures above.
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<table class="tdsr__table tdsr__table--inner">
|
||
<thead>
|
||
<tr>
|
||
<th>Quarter</th>
|
||
<th class="num">Interest</th>
|
||
<th class="num">Cumulative</th>
|
||
<th class="num">Liability to date</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">crossed here</span>
|
||
}
|
||
@if (line.declarationActive) {
|
||
<span class="tdsr__pill tdsr__pill--ok">Form 121 active</span>
|
||
}
|
||
</td>
|
||
<td class="num">₹{{ line.interest | number: '1.0-0' }}</td>
|
||
<td class="num">₹{{ line.cumInterest | number: '1.0-0' }}</td>
|
||
<td class="num">₹{{ line.cumLiability | number: '1.0-0' }}</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>
|
||
|
||
<div class="tdsr__meta">
|
||
<span
|
||
>Settlement: <strong>{{ row.mode === 'QUARTERLY' ? 'Quarterly' : 'Year end' }}</strong> (set by
|
||
the deposit product)</span
|
||
>
|
||
@if (row.declarationAcceptedOn) {
|
||
<span
|
||
>Declaration accepted: <strong>{{ row.declarationAcceptedOn | date: 'd MMM y' }}</strong></span
|
||
>
|
||
}
|
||
@if (row.paidWithoutDeduction) {
|
||
<span>
|
||
Paid without deduction:
|
||
<strong>₹{{ row.paidWithoutDeduction | number: '1.0-0' }}</strong> — reportable in
|
||
{{ statute.statement }}
|
||
</span>
|
||
}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
}
|
||
}
|
||
@if (visibleRows.length === 0) {
|
||
<tr>
|
||
<td colspan="13" class="tdsr__empty">
|
||
<fa-icon icon="info-circle"></fa-icon> No customers in this section match the 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>
|
||
{{ selectedTab === 'paying' ? 'deductees paying TDS' : 'deductees not paying TDS' }}
|
||
</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>
|