feat: add 11 banking compliance & accounting report modules

Adds dialog components and feature modules for RBI/banking compliance,
KYC, and accounting reports. Wires them into the shell menu (RBI,
General Ledger, CKYC Mgmt, Others, Insurance) and dashboard cards.

Dialog components (lazy-loaded MatDialog):
- PSL Returns (Form III) — dashboard card + RBI menu
- ALM Statement of Structural Liquidity (Form ALM-1) — RBI menu
- CIMS / XBRL Returns (OSS-2 / PSA / RAL / RCP-UCB) — RBI menu, shared
  dialog with form-pill switcher
- CIC DQR (CIBIL / Experian / Equifax / CRIF, 72-col TUDF) — Others menu
  + dashboard card
- CKYC Compliance Reports (Re-KYC / Risk Review / CERSAI) — CKYC Mgmt
- Accounting Reports (P&L / Balance Sheet / Comparative B/S / GL DR/CR
  / Misc Expense Ledger) — General Ledger menu

Feature modules (Route.withShell):
- DEAF (/deaf): Form I, Form II, Annex 4, accounts register
- DICGC (/dicgc): Slab Report, SAHAY Depositor List
- FIU-IND (/fiu): FINnet 2.0 Statement of Financial Transactions
- SFT (/sft): Income Tax Section 285BA SFT
- CKYC Reports (/ckyc/ckyc-reports): standalone dashboard

UI:
- Theme-colored icons on 22 wired menu links across 5 menus —
  visually distinguishes wired (colored) from placeholder (gray) links.
- Backdrop classes per dialog in main.scss.

All using dummy data (Bharat Urban Bank Ltd, Karthik Kamath as Principal
Officer, dummy customer names) — no real bank or customer references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-29 18:59:50 +05:30
co-authored by Claude Opus 4.7
parent 634003a75c
commit 12bfc81219
61 changed files with 22207 additions and 725 deletions
+654
View File
@@ -0,0 +1,654 @@
/**
* 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/.
*/
/**
* SFT — Statement of Financial Transactions, filed under
* Section 285BA of the Income Tax Act 1961 + Rule 114E,
* with the Income Tax Reporting Portal (report.insight.gov.in)
* by 31 May for the previous FY.
*
* Schema reproduces the actual SFT CSV templates; bank/customer names are dummy.
*
* SFT-001 Cash purchase of bank drafts / pay orders / banker's cheques (≥ ₹10 L cash)
* SFT-002 Time-deposit (FD/RD) opening (≥ ₹10 L cash)
* SFT-003 Cash deposits / withdrawals — current account (≥ ₹50 L per FY)
* SFT-004 Cash deposits — savings account (≥ ₹10 L per FY)
* SFT-015 Dividend payments
* SFT-016 Interest paid on deposits
*/
export type SftCode = 'SFT-001' | 'SFT-002' | 'SFT-003' | 'SFT-004' | 'SFT-015' | 'SFT-016';
/** Person-wise schema (SFT-001 / 002 / 004) */
export interface SftPersonRecord {
reportSerialNumber: number;
originalReportSerialNumber: number;
personName: string;
personType: string; // I=Individual, F=Firm, P=Partnership, etc.
customerIdentity: string;
gender: 'M' | 'F' | 'O' | '';
fatherName: string;
pan: string;
aadhaar: string;
form60Ack: string;
identificationType: string;
identificationNumber: string;
dobOrIncorporation: string;
nationality: string;
addressType: string;
fixAddress: string;
city: string;
postCode: string;
state: string;
countryCode: string;
businessOrOccupation: string;
primaryStdCode: string;
primaryPhoneNo: string;
primaryMobileNo: string;
email: string;
remarks: string;
productType: string; // SFT-001/002/004 — DD / Pay Order / Time Deposit / SB
aggGrossReceived: number;
aggGrossReceivedCash: number;
aggGrossPaid: number;
productIdentifier: string;
lastDateOfTransaction: string;
amountReceivedFromPerson: number;
cashAmountReceivedFromPerson: number;
amountPaidToPerson: number;
relatedAccountNumber: string;
relatedInstituteName: string;
relatedInstituteRefNumber: string;
/** Which SFT code this row belongs to */
sftCode: SftCode;
}
/** Account-wise schema with embedded Person Card (SFT-003) */
export interface SftAccountRecord {
reportSerialNumber: number;
originalReportSerialNumber: number;
accountType: string;
accountNumber: string;
accountHolderName: string;
accountStatus: 'A' | 'C' | 'D';
branchReferenceNumber: string;
branchName: string;
branchAddress: string;
branchCity: string;
branchPostalCode: string;
branchState: string;
branchCountry: string;
branchStdCode: string;
branchPhone: string;
branchMobile: string;
branchFaxStd: string;
branchFaxPhone: string;
branchEmail: string;
aggGrossCashCredited: number;
aggGrossCashDebited: number;
amountCreditedBeforeDemonetization: number;
amountCreditedAfterDemonetization: number;
// Person Card (PC)
pcAccountRelationship: string;
pcPersonName: string;
pcPersonType: string;
pcCustomerIdentity: string;
pcGender: 'M' | 'F' | 'O' | '';
pcFatherName: string;
pcPan: string;
pcAadhaar: string;
pcForm60Ack: string;
pcIdentificationType: string;
pcIdentificationNumber: string;
pcDob: string;
pcNationality: string;
pcBusiness: string;
pcAddressType: string;
pcAddress: string;
pcCity: string;
pcPin: string;
pcState: string;
pcCountry: string;
pcPrimaryStd: string;
pcPrimaryPhone: string;
pcPrimaryMobile: string;
pcEmail: string;
}
/** Compact pipe-delimited schema (SFT-015 dividend / SFT-016 interest) */
export interface SftInterestDividendRecord {
rsn: number;
fy: string;
name: string;
pan: string;
aadhaar: string;
mobile: string;
email: string;
/** Address only present in SFT-015 */
address?: string;
pincode?: string;
/** Account fields only present in SFT-016 */
accountNumber?: string;
typeOfAccount?: string;
/** SFT-015 = dividend amount; SFT-016 = interest paid */
amount: number;
sftCode: 'SFT-015' | 'SFT-016';
}
export const SFT_BANK = {
name: 'Bharat Urban Bank Ltd',
branch: 'Head Office',
filerPan: 'AABCB1100M',
filerTan: 'MUMR99001E',
itdReportingPortalId: 'BHARATURBAN-285BA',
fy: '2025-26',
filingDueDate: '31-05-2026',
principalOfficer: 'Karthik Kamath',
poDesignation: 'Chief Compliance Officer',
branchRef: 'YESB0BHRT',
branchAddress: '12 MG Road, Worli',
branchCity: 'MUMBAI',
branchState: 'MH',
branchPostalCode: '400018',
branchCountry: 'IN',
branchStdCode: '022',
branchPhone: '24938000',
branchMobile: '9892000100',
branchEmail: 'compliance@bharaturbanbank.example'
};
/** ── SFT-001 / 002 / 004 — Person-wise records ── */
export const SFT_PERSON_RECORDS: SftPersonRecord[] = [
// SFT-001 — Cash purchase of DD ≥ ₹10 L
{
reportSerialNumber: 1,
originalReportSerialNumber: 0,
personName: 'RAVI KUMAR SHARMA',
personType: 'I',
customerIdentity: '10078',
gender: 'M',
fatherName: 'RAJESH KUMAR SHARMA',
pan: 'AAQPS8560J',
aadhaar: '',
form60Ack: '',
identificationType: 'C',
identificationNumber: 'AAQPS8560J',
dobOrIncorporation: '10-08-1985',
nationality: 'IN',
addressType: '2',
fixAddress: 'FLAT 402 SUNRISE TOWERS MAROL ANDHERI EAST',
city: 'MUMBAI',
postCode: '400069',
state: 'MH',
countryCode: 'IN',
businessOrOccupation: 'SERVICE',
primaryStdCode: '',
primaryPhoneNo: '',
primaryMobileNo: '9892011001',
email: 'ravi.sharma@example.com',
remarks: '',
productType: 'BANK_DRAFT',
aggGrossReceived: 1500000,
aggGrossReceivedCash: 1500000,
aggGrossPaid: 0,
productIdentifier: 'DD24304',
lastDateOfTransaction: '20-01-2026',
amountReceivedFromPerson: 1500000,
cashAmountReceivedFromPerson: 1500000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001101000838',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-001'
},
{
reportSerialNumber: 2,
originalReportSerialNumber: 0,
personName: 'PRIYA PATEL',
personType: 'I',
customerIdentity: '10079',
gender: 'F',
fatherName: 'HARISH PATEL',
pan: 'AKLPS9933P',
aadhaar: '',
form60Ack: '',
identificationType: 'C',
identificationNumber: 'AKLPS9933P',
dobOrIncorporation: '22-03-1990',
nationality: 'IN',
addressType: '2',
fixAddress: 'B-12 SHIVAJI NAGAR FC ROAD',
city: 'PUNE',
postCode: '411005',
state: 'MH',
countryCode: 'IN',
businessOrOccupation: 'BUSINESS',
primaryStdCode: '',
primaryPhoneNo: '',
primaryMobileNo: '9822055002',
email: 'priya.patel@example.com',
remarks: '',
productType: 'BANKERS_CHEQUE',
aggGrossReceived: 1100000,
aggGrossReceivedCash: 1100000,
aggGrossPaid: 0,
productIdentifier: 'BC100039',
lastDateOfTransaction: '02-01-2026',
amountReceivedFromPerson: 1100000,
cashAmountReceivedFromPerson: 1100000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001352000357',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-001'
},
// SFT-002 — Time deposit ≥ ₹10 L cash
{
reportSerialNumber: 3,
originalReportSerialNumber: 0,
personName: 'VIKRAM REDDY',
personType: 'I',
customerIdentity: '20101',
gender: 'M',
fatherName: 'SURESH REDDY',
pan: 'AAEPR9352L',
aadhaar: '',
form60Ack: '',
identificationType: 'C',
identificationNumber: 'AAEPR9352L',
dobOrIncorporation: '07-07-1982',
nationality: 'IN',
addressType: '2',
fixAddress: 'NO 23 4TH CROSS JAYANAGAR 5TH BLOCK',
city: 'BENGALURU',
postCode: '560041',
state: 'KA',
countryCode: 'IN',
businessOrOccupation: 'PROFESSION',
primaryStdCode: '',
primaryPhoneNo: '',
primaryMobileNo: '9845022004',
email: '',
remarks: '',
productType: 'TIME_DEPOSIT',
aggGrossReceived: 2500000,
aggGrossReceivedCash: 2500000,
aggGrossPaid: 0,
productIdentifier: 'FDR-2026-001',
lastDateOfTransaction: '15-09-2025',
amountReceivedFromPerson: 2500000,
cashAmountReceivedFromPerson: 2500000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001352000420',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-002'
},
{
reportSerialNumber: 4,
originalReportSerialNumber: 0,
personName: 'NIRAV SHAH',
personType: 'I',
customerIdentity: '40301',
gender: 'M',
fatherName: 'JAYESH SHAH',
pan: 'AHKPS5544Z',
aadhaar: '',
form60Ack: '',
identificationType: 'C',
identificationNumber: 'AHKPS5544Z',
dobOrIncorporation: '18-05-1992',
nationality: 'IN',
addressType: '2',
fixAddress: 'A-401 ATLANTIS APARTMENTS SATELLITE ROAD',
city: 'AHMEDABAD',
postCode: '380015',
state: 'GJ',
countryCode: 'IN',
businessOrOccupation: 'SERVICE',
primaryStdCode: '',
primaryPhoneNo: '',
primaryMobileNo: '9824012010',
email: 'nirav.shah@example.com',
remarks: '',
productType: 'TIME_DEPOSIT',
aggGrossReceived: 1800000,
aggGrossReceivedCash: 1800000,
aggGrossPaid: 0,
productIdentifier: 'FDR-2026-002',
lastDateOfTransaction: '12-12-2025',
amountReceivedFromPerson: 1800000,
cashAmountReceivedFromPerson: 1800000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001352000511',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-002'
},
// SFT-004 — Cash deposits in SB ≥ ₹10 L per FY
{
reportSerialNumber: 5,
originalReportSerialNumber: 0,
personName: 'KAVITA VERMA',
personType: 'I',
customerIdentity: '50402',
gender: 'F',
fatherName: 'DEEPAK VERMA',
pan: 'AKHPV2244R',
aadhaar: '',
form60Ack: '',
identificationType: 'C',
identificationNumber: 'AKHPV2244R',
dobOrIncorporation: '01-04-1988',
nationality: 'IN',
addressType: '2',
fixAddress: '45 CIVIL LINES MALL ROAD',
city: 'KANPUR',
postCode: '208001',
state: 'UP',
countryCode: 'IN',
businessOrOccupation: 'SERVICE',
primaryStdCode: '',
primaryPhoneNo: '',
primaryMobileNo: '9415055014',
email: '',
remarks: '',
productType: 'SAVINGS_DEPOSIT',
aggGrossReceived: 1250000,
aggGrossReceivedCash: 1250000,
aggGrossPaid: 0,
productIdentifier: 'SB-50402',
lastDateOfTransaction: '28-01-2026',
amountReceivedFromPerson: 1250000,
cashAmountReceivedFromPerson: 1250000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001101001145',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-004'
},
{
reportSerialNumber: 6,
originalReportSerialNumber: 0,
personName: 'SAMEER KAPOOR',
personType: 'I',
customerIdentity: '70601',
gender: 'M',
fatherName: 'RAJINDER KAPOOR',
pan: 'AHKPK7788Q',
aadhaar: '',
form60Ack: '',
identificationType: 'C',
identificationNumber: 'AHKPK7788Q',
dobOrIncorporation: '03-02-1987',
nationality: 'IN',
addressType: '2',
fixAddress: 'B-148 GREATER KAILASH GK PART 1',
city: 'NEW DELHI',
postCode: '110048',
state: 'DL',
countryCode: 'IN',
businessOrOccupation: 'SERVICE',
primaryStdCode: '',
primaryPhoneNo: '',
primaryMobileNo: '9811022019',
email: 'sameer.kapoor@example.com',
remarks: '',
productType: 'SAVINGS_DEPOSIT',
aggGrossReceived: 2150000,
aggGrossReceivedCash: 2150000,
aggGrossPaid: 0,
productIdentifier: 'SB-70601',
lastDateOfTransaction: '15-01-2026',
amountReceivedFromPerson: 2150000,
cashAmountReceivedFromPerson: 2150000,
amountPaidToPerson: 0,
relatedAccountNumber: '110001101001458',
relatedInstituteName: 'BHARAT URBAN BANK',
relatedInstituteRefNumber: 'YESB0BHRT',
sftCode: 'SFT-004'
}
];
/** ── SFT-003 — Account-wise (current account) cash deposits ≥ ₹50 L per FY ── */
export const SFT_ACCOUNT_RECORDS: SftAccountRecord[] = [
{
reportSerialNumber: 1,
originalReportSerialNumber: 0,
accountType: 'BC',
accountNumber: '110001101000685',
accountHolderName: 'NEXTGEN TECH PVT LTD',
accountStatus: 'A',
branchReferenceNumber: 'YESB0BHRT',
branchName: 'HEAD OFFICE',
branchAddress: '12 MG ROAD WORLI MUMBAI 400018',
branchCity: 'MUMBAI',
branchPostalCode: '400018',
branchState: 'MH',
branchCountry: 'IN',
branchStdCode: '022',
branchPhone: '24938000',
branchMobile: '9892000100',
branchFaxStd: '022',
branchFaxPhone: '24938011',
branchEmail: 'compliance@bharaturbanbank.example',
aggGrossCashCredited: 6500000,
aggGrossCashDebited: 4200000,
amountCreditedBeforeDemonetization: 0,
amountCreditedAfterDemonetization: 0,
pcAccountRelationship: 'F',
pcPersonName: 'NEXTGEN TECH PVT LTD',
pcPersonType: 'PF',
pcCustomerIdentity: 'E10102',
pcGender: '',
pcFatherName: '',
pcPan: 'AABCN5566K',
pcAadhaar: '',
pcForm60Ack: '',
pcIdentificationType: 'C',
pcIdentificationNumber: 'AABCN5566K',
pcDob: '18-09-2015',
pcNationality: 'IN',
pcBusiness: 'IT_SERVICES',
pcAddressType: '2',
pcAddress: 'TOWER B SUNRISE BUSINESS PARK ANDHERI EAST',
pcCity: 'MUMBAI',
pcPin: '400093',
pcState: 'MH',
pcCountry: 'IN',
pcPrimaryStd: '',
pcPrimaryPhone: '',
pcPrimaryMobile: '9824012011',
pcEmail: ''
},
{
reportSerialNumber: 2,
originalReportSerialNumber: 0,
accountType: 'BC',
accountNumber: '110001101000922',
accountHolderName: 'BHARAT TRADING COMPANY (PROP. RAVI KUMAR SHARMA)',
accountStatus: 'A',
branchReferenceNumber: 'YESB0BHRT',
branchName: 'HEAD OFFICE',
branchAddress: '12 MG ROAD WORLI MUMBAI 400018',
branchCity: 'MUMBAI',
branchPostalCode: '400018',
branchState: 'MH',
branchCountry: 'IN',
branchStdCode: '022',
branchPhone: '24938000',
branchMobile: '9892000100',
branchFaxStd: '022',
branchFaxPhone: '24938011',
branchEmail: 'compliance@bharaturbanbank.example',
aggGrossCashCredited: 5800000,
aggGrossCashDebited: 3450000,
amountCreditedBeforeDemonetization: 0,
amountCreditedAfterDemonetization: 0,
pcAccountRelationship: 'F',
pcPersonName: 'BHARAT TRADING COMPANY',
pcPersonType: 'PF',
pcCustomerIdentity: 'E10101',
pcGender: '',
pcFatherName: '',
pcPan: 'ABCFB1234N',
pcAadhaar: '',
pcForm60Ack: '',
pcIdentificationType: 'C',
pcIdentificationNumber: 'ABCFB1234N',
pcDob: '24-03-2008',
pcNationality: 'IN',
pcBusiness: 'TRADING',
pcAddressType: '2',
pcAddress: 'GALA 22 LOWER PAREL',
pcCity: 'MUMBAI',
pcPin: '400013',
pcState: 'MH',
pcCountry: 'IN',
pcPrimaryStd: '',
pcPrimaryPhone: '',
pcPrimaryMobile: '9892011010',
pcEmail: ''
}
];
/** ── SFT-015 / SFT-016 — Pipe-delimited interest & dividend ── */
export const SFT_INT_DIV_RECORDS: SftInterestDividendRecord[] = [
// SFT-016 — Interest paid to depositors
{
rsn: 1,
fy: '2025-26',
name: 'RAVI KUMAR SHARMA',
pan: 'AAQPS8560J',
aadhaar: 'XXXXXXXX1234',
mobile: '9892011001',
email: 'ravi.sharma@example.com',
accountNumber: '110001302000312',
typeOfAccount: 'FD',
amount: 56250,
sftCode: 'SFT-016'
},
{
rsn: 2,
fy: '2025-26',
name: 'PRIYA PATEL',
pan: 'AKLPS9933P',
aadhaar: 'XXXXXXXX2210',
mobile: '9822055002',
email: 'priya.patel@example.com',
accountNumber: '110001302000418',
typeOfAccount: 'FD',
amount: 41280,
sftCode: 'SFT-016'
},
{
rsn: 3,
fy: '2025-26',
name: 'VIKRAM REDDY',
pan: 'AAEPR9352L',
aadhaar: 'XXXXXXXX6622',
mobile: '9845022004',
email: '',
accountNumber: '110001302000711',
typeOfAccount: 'FD',
amount: 73500,
sftCode: 'SFT-016'
},
{
rsn: 4,
fy: '2025-26',
name: 'LAKSHMI IYER',
pan: '',
aadhaar: 'XXXXXXXX5511',
mobile: '9845066005',
email: '',
accountNumber: '110001301000814',
typeOfAccount: 'SB',
amount: 18420,
sftCode: 'SFT-016'
},
{
rsn: 5,
fy: '2025-26',
name: 'NIRAV SHAH',
pan: 'AHKPS5544Z',
aadhaar: 'XXXXXXXX9933',
mobile: '9824012010',
email: 'nirav.shah@example.com',
accountNumber: '110001352000511',
typeOfAccount: 'FD',
amount: 124200,
sftCode: 'SFT-016'
},
{
rsn: 6,
fy: '2025-26',
name: 'KAVITA VERMA',
pan: 'AKHPV2244R',
aadhaar: 'XXXXXXXX1144',
mobile: '9415055014',
email: '',
accountNumber: '110001101001145',
typeOfAccount: 'SB',
amount: 9850,
sftCode: 'SFT-016'
},
{
rsn: 7,
fy: '2025-26',
name: 'SAMEER KAPOOR',
pan: 'AHKPK7788Q',
aadhaar: 'XXXXXXXX8877',
mobile: '9811022019',
email: 'sameer.kapoor@example.com',
accountNumber: '110001304001458',
typeOfAccount: 'TL',
amount: 285600,
sftCode: 'SFT-016'
},
// SFT-015 — Dividend payments (bank as paying entity, e.g. on shares held)
{
rsn: 8,
fy: '2025-26',
name: 'ABHISHEK BANERJEE',
pan: 'AHKPK7788Q',
aadhaar: 'XXXXXXXX3344',
mobile: '9831022021',
email: 'abhishek.b@example.com',
address: '14/B SALT LAKE SECTOR V BIDHANNAGAR',
pincode: '700091',
amount: 24500,
sftCode: 'SFT-015'
},
{
rsn: 9,
fy: '2025-26',
name: 'GAURAV THAKUR',
pan: 'AILPT4455G',
aadhaar: 'XXXXXXXX5566',
mobile: '9418022023',
email: '',
address: 'HOUSE 7 NEW SHIMLA SECTOR 4',
pincode: '171002',
amount: 18750,
sftCode: 'SFT-015'
},
{
rsn: 10,
fy: '2025-26',
name: 'MANMOHAN SINGH GILL',
pan: 'ANBPG2233J',
aadhaar: '',
mobile: '9876022027',
email: 'manmohan.gill@example.com',
address: 'H 32 MODEL TOWN MAJITHA ROAD AMRITSAR',
pincode: '143001',
amount: 35200,
sftCode: 'SFT-015'
}
];
+29
View File
@@ -0,0 +1,29 @@
/**
* 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 { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route } from '../core/route/route.service';
import { SftComponent } from './sft.component';
const routes: Routes = [
Route.withShell([
{
path: 'sft',
component: SftComponent,
data: { title: 'Income Tax — SFT', breadcrumb: 'SFT Reports' }
}
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SftRoutingModule {}
+397
View File
@@ -0,0 +1,397 @@
<!--
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="sft-root">
<!-- HEADER -->
<div class="sft-head">
<div class="sft-head-left">
<fa-icon icon="file-invoice" class="sft-head-ic"></fa-icon>
<div>
<h2 class="sft-head-t">Income Tax — Statement of Financial Transactions (SFT)</h2>
<div class="sft-head-s">
{{ bank.name }} &middot; {{ bank.branch }} &middot; FY <strong>{{ bank.fy }}</strong> &middot; IT Act §285BA /
Rule 114E &middot; Due <strong>{{ bank.filingDueDate }}</strong>
</div>
</div>
</div>
<div class="sft-head-meta">
<div>
<span class="sft-meta-l">Filer PAN</span><span class="sft-meta-v mono">{{ bank.filerPan }}</span>
</div>
<div>
<span class="sft-meta-l">TAN</span><span class="sft-meta-v mono">{{ bank.filerTan }}</span>
</div>
<div>
<span class="sft-meta-l">Principal Officer</span><span class="sft-meta-v">{{ bank.principalOfficer }}</span>
</div>
</div>
</div>
<!-- TABS -->
<div class="sft-tabs">
<button class="sft-tab" [class.active]="activeTab === 'dashboard'" (click)="showTab('dashboard')">
<fa-icon icon="chart-pie"></fa-icon> Dashboard
</button>
<button class="sft-tab" [class.active]="activeTab === 'person'" (click)="showTab('person')">
<fa-icon icon="users"></fa-icon> Person-wise (001 / 002 / 004)
</button>
<button class="sft-tab" [class.active]="activeTab === 'account'" (click)="showTab('account')">
<fa-icon icon="university"></fa-icon> Account-wise (003)
</button>
<button class="sft-tab" [class.active]="activeTab === 'intdiv'" (click)="showTab('intdiv')">
<fa-icon icon="coins"></fa-icon> Interest &amp; Dividend (015 / 016)
</button>
</div>
<!-- DASHBOARD -->
@if (activeTab === 'dashboard') {
<div class="sft-section">
<div class="sft-kpis">
<div class="sft-kpi sft-kpi--blue">
<div class="sft-kpi-ic"><fa-icon icon="file-alt"></fa-icon></div>
<div>
<div class="sft-kpi-n">{{ totalRecords | number }}</div>
<div class="sft-kpi-l">Total Records</div>
<div class="sft-kpi-sub">all SFT codes combined</div>
</div>
</div>
<div class="sft-kpi sft-kpi--teal">
<div class="sft-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="sft-kpi-n">&#8377;{{ totalReportableAmount | number: '1.0-0' }}</div>
<div class="sft-kpi-l">Reportable Amount</div>
<div class="sft-kpi-sub">aggregate per FY {{ bank.fy }}</div>
</div>
</div>
<div class="sft-kpi sft-kpi--amber">
<div class="sft-kpi-ic"><fa-icon icon="users"></fa-icon></div>
<div>
<div class="sft-kpi-n">{{ sft001Count + sft002Count + sft004Count | number }}</div>
<div class="sft-kpi-l">Person-wise Records</div>
<div class="sft-kpi-sub">
{{ sft001Count }} ·001 &middot; {{ sft002Count }} ·002 &middot; {{ sft004Count }} ·004
</div>
</div>
</div>
<div class="sft-kpi sft-kpi--purple">
<div class="sft-kpi-ic"><fa-icon icon="university"></fa-icon></div>
<div>
<div class="sft-kpi-n">{{ sft003Count | number }}</div>
<div class="sft-kpi-l">SFT-003 Accounts</div>
<div class="sft-kpi-sub">cash CA ≥ &#8377;50 L per FY</div>
</div>
</div>
<div class="sft-kpi sft-kpi--green">
<div class="sft-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="sft-kpi-n">{{ sft015Count + sft016Count | number }}</div>
<div class="sft-kpi-l">Int / Dividend</div>
<div class="sft-kpi-sub">{{ sft015Count }} ·015 &middot; {{ sft016Count }} ·016</div>
</div>
</div>
</div>
<!-- SFT code summary cards -->
<div class="sft-codes">
<div class="sft-code-card">
<div class="sft-code-h">
<span class="sft-code-tag t-001">SFT-001</span><span>Cash Drafts / Pay Orders</span>
</div>
<p>
Cash purchase of bank drafts, pay orders or banker's cheques aggregating ≥ <strong>&#8377;10 L</strong> per
person/FY
</p>
<div class="sft-code-meta">{{ sft001Count }} record{{ sft001Count === 1 ? '' : 's' }}</div>
</div>
<div class="sft-code-card">
<div class="sft-code-h">
<span class="sft-code-tag t-002">SFT-002</span><span>Time Deposits (FD/RD)</span>
</div>
<p>Time-deposit opening with cash ≥ <strong>&#8377;10 L</strong> per person/FY (excluding renewals)</p>
<div class="sft-code-meta">{{ sft002Count }} record{{ sft002Count === 1 ? '' : 's' }}</div>
</div>
<div class="sft-code-card">
<div class="sft-code-h"><span class="sft-code-tag t-003">SFT-003</span><span>Current Account Cash</span></div>
<p>
Cash deposits / withdrawals — <strong>current account</strong> aggregating ≥
<strong>&#8377;50 L</strong> per FY
</p>
<div class="sft-code-meta">{{ sft003Count }} account{{ sft003Count === 1 ? '' : 's' }}</div>
</div>
<div class="sft-code-card">
<div class="sft-code-h"><span class="sft-code-tag t-004">SFT-004</span><span>Savings Bank Cash</span></div>
<p>Cash deposits — <strong>savings account</strong> aggregating ≥ <strong>&#8377;10 L</strong> per FY</p>
<div class="sft-code-meta">{{ sft004Count }} record{{ sft004Count === 1 ? '' : 's' }}</div>
</div>
<div class="sft-code-card">
<div class="sft-code-h"><span class="sft-code-tag t-015">SFT-015</span><span>Dividend Payments</span></div>
<p>Dividend payments to shareholders — any amount, aggregated per PAN/FY</p>
<div class="sft-code-meta">{{ sft015Count }} record{{ sft015Count === 1 ? '' : 's' }}</div>
</div>
<div class="sft-code-card">
<div class="sft-code-h">
<span class="sft-code-tag t-016">SFT-016</span><span>Interest Paid on Deposits</span>
</div>
<p>Interest paid on deposits — any amount, aggregated per PAN/FY</p>
<div class="sft-code-meta">{{ sft016Count }} record{{ sft016Count === 1 ? '' : 's' }}</div>
</div>
</div>
<div class="sft-toolbar">
<button class="sft-btn sft-btn-xls" (click)="exportAll()">
<fa-icon icon="file-csv"></fa-icon> Download All SFT Files
</button>
</div>
</div>
}
<!-- PERSON-WISE -->
@if (activeTab === 'person') {
<div class="sft-section">
<div class="sft-form-meta">
<div class="sft-form-title">
<h3>Person-wise Records — SFT-001 / 002 / 004</h3>
<p>One record per person per SFT code per FY (aggregate)</p>
</div>
<div class="sft-action-group">
<select class="sft-inp sft-inp-narrow" [(ngModel)]="personSftFilter">
<option value="ALL">All Codes</option>
<option value="SFT-001">SFT-001 only</option>
<option value="SFT-002">SFT-002 only</option>
<option value="SFT-004">SFT-004 only</option>
</select>
<button class="sft-btn sft-btn-csv" (click)="exportPersonCsv('SFT-001')">
<fa-icon icon="file-csv"></fa-icon> SFT_001.csv
</button>
<button class="sft-btn sft-btn-csv" (click)="exportPersonCsv('SFT-002')">
<fa-icon icon="file-csv"></fa-icon> SFT_002.csv
</button>
<button class="sft-btn sft-btn-csv" (click)="exportPersonCsv('SFT-004')">
<fa-icon icon="file-csv"></fa-icon> SFT_004.csv
</button>
</div>
</div>
<div class="sft-tbl-wrap">
<table class="sft-tbl">
<thead>
<tr>
<th class="ta-c">RSN</th>
<th>Code</th>
<th>Person Name</th>
<th class="ta-c">Type</th>
<th>PAN</th>
<th>DOB</th>
<th>Mobile</th>
<th>City</th>
<th>Product</th>
<th class="ta-r">Aggregate</th>
<th class="ta-r">Cash</th>
<th>Last Txn</th>
<th>Account</th>
</tr>
</thead>
<tbody>
@for (r of filteredPersons; track r.reportSerialNumber + '-' + r.sftCode) {
<tr>
<td class="ta-c">{{ r.reportSerialNumber }}</td>
<td>
<span class="sft-code-tag" [class]="'t-' + r.sftCode.replace('SFT-', '')">{{ r.sftCode }}</span>
</td>
<td>{{ r.personName }}</td>
<td class="ta-c">{{ r.personType }}</td>
<td class="mono">{{ r.pan }}</td>
<td>{{ r.dobOrIncorporation }}</td>
<td class="mono">{{ r.primaryMobileNo }}</td>
<td>{{ r.city }}, {{ r.state }}</td>
<td>
<span class="muted">{{ productLabel(r.productType) }}</span>
</td>
<td class="ta-r mono">&#8377;{{ r.aggGrossReceived | number: '1.0-0' }}</td>
<td class="ta-r mono">&#8377;{{ r.aggGrossReceivedCash | number: '1.0-0' }}</td>
<td>{{ r.lastDateOfTransaction }}</td>
<td class="mono">{{ r.relatedAccountNumber }}</td>
</tr>
} @empty {
<tr>
<td colspan="13" class="sft-empty">No records for the selected SFT code.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- ACCOUNT-WISE (SFT-003) -->
@if (activeTab === 'account') {
<div class="sft-section">
<div class="sft-form-meta">
<div class="sft-form-title">
<h3>SFT-003 — Cash Deposits/Withdrawals in Current Accounts</h3>
<p>Account-wise schema with embedded Person Card; aggregate ≥ &#8377;50 L cash per FY</p>
</div>
<button class="sft-btn sft-btn-csv" (click)="exportAccountCsv()">
<fa-icon icon="file-csv"></fa-icon> Download SFT_003.csv
</button>
</div>
<div class="sft-acc-list">
@for (r of accountRecords; track r.reportSerialNumber) {
<div class="acc-card">
<div class="acc-card-h">
<div class="acc-card-id">
<span class="acc-rsn">RSN-{{ r.reportSerialNumber }}</span>
<span class="acc-type-pill">{{ r.accountType }}</span>
<span class="acc-status-pill" [class.act]="r.accountStatus === 'A'">{{
r.accountStatus === 'A' ? 'Active' : r.accountStatus === 'C' ? 'Closed' : 'Dormant'
}}</span>
</div>
<div class="acc-card-amt">
<span class="muted">Cash credited</span>
<strong>&#8377;{{ r.aggGrossCashCredited | number: '1.0-0' }}</strong>
<span class="muted">/ debited</span>
<strong>&#8377;{{ r.aggGrossCashDebited | number: '1.0-0' }}</strong>
</div>
</div>
<div class="acc-card-body">
<div class="acc-meta">
<div>
<label>Account No.</label><span class="mono">{{ r.accountNumber }}</span>
</div>
<div>
<label>Holder</label><span>{{ r.accountHolderName }}</span>
</div>
<div>
<label>Branch</label><span>{{ r.branchName }} ({{ r.branchReferenceNumber }})</span>
</div>
<div>
<label>Branch Address</label><span>{{ r.branchAddress }}</span>
</div>
<div>
<label>Branch Phone</label><span class="mono">{{ r.branchStdCode }}-{{ r.branchPhone }}</span>
</div>
<div>
<label>Branch Email</label><span>{{ r.branchEmail }}</span>
</div>
</div>
<div class="acc-pc-block">
<div class="acc-pc-h"><fa-icon icon="user"></fa-icon> Person Card</div>
<div class="acc-meta">
<div>
<label>Name</label><span>{{ r.pcPersonName }}</span>
</div>
<div>
<label>Type</label><span>{{ r.pcPersonType }}</span>
</div>
<div>
<label>PAN</label><span class="mono">{{ r.pcPan }}</span>
</div>
<div>
<label>DOB / Incorp.</label><span>{{ r.pcDob }}</span>
</div>
<div>
<label>Business</label><span>{{ productLabel(r.pcBusiness) }}</span>
</div>
<div>
<label>Mobile</label><span class="mono">{{ r.pcPrimaryMobile }}</span>
</div>
<div class="acc-pc-addr">
<label>Address</label><span>{{ r.pcAddress }}, {{ r.pcCity }}, {{ r.pcState }} {{ r.pcPin }}</span>
</div>
</div>
</div>
</div>
</div>
} @empty {
<div class="sft-empty-card">No SFT-003 records this FY.</div>
}
</div>
</div>
}
<!-- INTEREST & DIVIDEND -->
@if (activeTab === 'intdiv') {
<div class="sft-section">
<div class="sft-form-meta">
<div class="sft-form-title">
<h3>SFT-015 (Dividend) &amp; SFT-016 (Interest Paid)</h3>
<p>Pipe-delimited compact format submitted to Income Tax Reporting Portal</p>
</div>
<div class="sft-action-group">
<select class="sft-inp sft-inp-narrow" [(ngModel)]="intDivFilter">
<option value="ALL">Both</option>
<option value="SFT-015">SFT-015 (Dividend)</option>
<option value="SFT-016">SFT-016 (Interest)</option>
</select>
<button class="sft-btn sft-btn-csv" (click)="exportIntDivCsv('SFT-015')">
<fa-icon icon="file-csv"></fa-icon> SFT15.csv
</button>
<button class="sft-btn sft-btn-csv" (click)="exportIntDivCsv('SFT-016')">
<fa-icon icon="file-csv"></fa-icon> SFT16.csv
</button>
</div>
</div>
<div class="sft-tbl-wrap">
<table class="sft-tbl">
<thead>
<tr>
<th class="ta-c">RSN</th>
<th>Code</th>
<th>FY</th>
<th>Name</th>
<th>PAN</th>
<th>Aadhaar (mask)</th>
<th>Mobile</th>
<th>Account</th>
<th class="ta-c">Type</th>
<th class="ta-r">Amount</th>
</tr>
</thead>
<tbody>
@for (r of filteredIntDiv; track r.rsn + '-' + r.sftCode) {
<tr>
<td class="ta-c">{{ r.rsn }}</td>
<td>
<span class="sft-code-tag" [class]="'t-' + r.sftCode.replace('SFT-', '')">{{ r.sftCode }}</span>
</td>
<td>{{ r.fy }}</td>
<td>{{ r.name }}</td>
<td class="mono">{{ r.pan || '—' }}</td>
<td class="mono">{{ r.aadhaar || '—' }}</td>
<td class="mono">{{ r.mobile }}</td>
<td class="mono">{{ r.accountNumber || '—' }}</td>
<td class="ta-c">{{ r.typeOfAccount || (r.sftCode === 'SFT-015' ? 'DIV' : '—') }}</td>
<td class="ta-r mono">
<strong>&#8377;{{ r.amount | number: '1.0-0' }}</strong>
</td>
</tr>
} @empty {
<tr>
<td colspan="10" class="sft-empty">No records for the selected SFT code.</td>
</tr>
}
</tbody>
</table>
</div>
<div class="sft-note">
<fa-icon icon="info-circle"></fa-icon>
<span
><strong>Note:</strong> SFT-015 (Dividend) and SFT-016 (Interest) are pipe-delimited (`|`) files, one row per
PAN per FY with aggregated amounts. Submit via the IT Reporting Portal under {{ bank.principalOfficer }}'s
digital signature.</span
>
</div>
</div>
}
<!-- TOAST -->
<div class="sft-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
</div>
+689
View File
@@ -0,0 +1,689 @@
/**
* 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 {
--s-navy: #0b1e3d;
--s-blue: #1d4ed8;
--s-blue-l: #eff6ff;
--s-teal: #0e7e6b;
--s-teal-l: #e0f4f0;
--s-green: #15803d;
--s-green-l: #dcfce7;
--s-amber: #d97706;
--s-amber-l: #fef3c7;
--s-red: #c0392b;
--s-red-l: #fdecea;
--s-purple: #6d28d9;
--s-purple-l: #ede9fe;
--s-pink: #db2777;
--s-pink-l: #fce7f3;
--s-g50: #f8fafc;
--s-g100: #f1f5f9;
--s-g200: #e2e8f0;
--s-g300: #cbd5e1;
--s-g500: #64748b;
--s-g700: #334155;
--s-g900: #0f172a;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
background: var(--s-g50);
color: var(--s-g900);
font-size: 14px;
line-height: 1.5;
}
.sft-root {
padding: 18px 22px;
max-width: 1500px;
margin: 0 auto;
}
.sft-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.sft-head-left {
display: flex;
align-items: center;
gap: 14px;
}
.sft-head-ic {
font-size: 28px;
color: var(--s-blue);
}
.sft-head-t {
margin: 0;
font-size: 18px;
font-weight: 700;
color: var(--s-g900);
}
.sft-head-s {
font-size: 12px;
color: var(--s-g500);
}
.sft-head-meta {
display: flex;
gap: 22px;
> div {
display: flex;
flex-direction: column;
.sft-meta-l {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--s-g500);
}
.sft-meta-v {
font-size: 14px;
font-weight: 600;
color: var(--s-g900);
}
}
}
.sft-tabs {
display: flex;
gap: 4px;
background: #fff;
border: 1px solid var(--s-g200);
border-radius: 10px;
padding: 4px;
margin-bottom: 16px;
overflow-x: auto;
}
.sft-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 500;
color: var(--s-g500);
cursor: pointer;
border-radius: 7px;
white-space: nowrap;
transition: all 0.15s;
font-family: inherit;
&:hover {
color: var(--s-g900);
}
&.active {
background: var(--s-blue);
color: #fff;
box-shadow: 0 2px 6px rgb(29 78 216 / 25%);
}
}
.sft-section {
display: flex;
flex-direction: column;
gap: 16px;
}
/* KPIs */
.sft-kpis {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.sft-kpi {
background: #fff;
border: 1px solid var(--s-g200);
border-radius: 12px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 14px rgb(0 0 0 / 10%);
}
}
.sft-kpi-ic {
width: 42px;
height: 42px;
border-radius: 11px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.sft-kpi--blue .sft-kpi-ic {
background: var(--s-blue-l);
color: var(--s-blue);
}
.sft-kpi--teal .sft-kpi-ic {
background: var(--s-teal-l);
color: var(--s-teal);
}
.sft-kpi--amber .sft-kpi-ic {
background: var(--s-amber-l);
color: var(--s-amber);
}
.sft-kpi--purple .sft-kpi-ic {
background: var(--s-purple-l);
color: var(--s-purple);
}
.sft-kpi--green .sft-kpi-ic {
background: var(--s-green-l);
color: var(--s-green);
}
.sft-kpi-n {
font-size: 18px;
font-weight: 700;
color: var(--s-g900);
line-height: 1.1;
}
.sft-kpi-l {
font-size: 12px;
color: var(--s-g700);
font-weight: 500;
margin-top: 2px;
}
.sft-kpi-sub {
font-size: 11px;
color: var(--s-g500);
margin-top: 1px;
}
/* SFT code summary cards */
.sft-codes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.sft-code-card {
background: #fff;
border: 1px solid var(--s-g200);
border-radius: 12px;
padding: 14px 16px;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.sft-code-h {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: var(--s-g900);
}
.sft-code-tag {
display: inline-block;
padding: 3px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
font-family: 'DM Mono', monospace;
background: var(--s-g100);
color: var(--s-g700);
&.t-001 {
background: var(--s-blue-l);
color: var(--s-blue);
}
&.t-002 {
background: var(--s-teal-l);
color: var(--s-teal);
}
&.t-003 {
background: var(--s-purple-l);
color: var(--s-purple);
}
&.t-004 {
background: var(--s-amber-l);
color: var(--s-amber);
}
&.t-015 {
background: var(--s-pink-l);
color: var(--s-pink);
}
&.t-016 {
background: var(--s-green-l);
color: var(--s-green);
}
}
.sft-code-card p {
margin: 0 0 10px;
font-size: 12px;
color: var(--s-g500);
line-height: 1.5;
}
.sft-code-meta {
font-size: 12px;
color: var(--s-g700);
font-weight: 600;
}
/* Toolbar */
.sft-toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* Form header */
.sft-form-meta {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 14px;
flex-wrap: wrap;
}
.sft-form-title {
h3 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: var(--s-g900);
}
p {
margin: 2px 0 0;
font-size: 12px;
color: var(--s-g500);
}
}
.sft-action-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* Buttons */
.sft-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid var(--s-g300);
background: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
color: var(--s-g700);
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
&:hover {
background: var(--s-g50);
border-color: var(--s-g500);
}
}
.sft-btn-csv {
background: #1d4ed8;
border-color: #1d4ed8;
color: #fff;
&:hover {
background: #1e40af;
border-color: #1e40af;
color: #fff;
}
}
.sft-btn-xls {
background: #16a34a;
border-color: #16a34a;
color: #fff;
&:hover {
background: #15803d;
border-color: #15803d;
color: #fff;
}
}
/* Inputs */
.sft-inp {
padding: 8px 12px;
border: 1px solid var(--s-g200);
border-radius: 8px;
font-size: 13px;
background: #fff;
font-family: inherit;
&:focus {
outline: none;
border-color: var(--s-blue);
box-shadow: 0 0 0 3px rgb(29 78 216 / 15%);
}
}
.sft-inp-narrow {
width: 200px;
}
/* Table */
.sft-tbl-wrap {
background: #fff;
border: 1px solid var(--s-g200);
border-radius: 12px;
overflow: auto;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.sft-tbl {
width: 100%;
border-collapse: collapse;
th {
text-align: left;
padding: 10px 12px;
font-size: 11px;
font-weight: 600;
color: var(--s-g500);
text-transform: uppercase;
letter-spacing: 0.04em;
background: var(--s-g50);
border-bottom: 1px solid var(--s-g200);
}
td {
padding: 10px 12px;
border-bottom: 1px solid var(--s-g100);
font-size: 12px;
color: var(--s-g700);
}
tr:last-child td {
border-bottom: none;
}
tr:hover td {
background: var(--s-g50);
}
}
.ta-c {
text-align: center;
}
.ta-r {
text-align: right;
}
.mono {
font-family: 'DM Mono', monospace;
font-size: 11px;
letter-spacing: 0.02em;
}
.muted {
color: var(--s-g500);
font-size: 11px;
}
.sft-empty {
text-align: center;
padding: 30px;
color: var(--s-g500);
font-style: italic;
}
.sft-empty-card {
background: #fff;
border: 1px dashed var(--s-g300);
border-radius: 12px;
padding: 50px;
text-align: center;
color: var(--s-g500);
font-size: 13px;
}
/* Account-wise SFT-003 cards */
.sft-acc-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.acc-card {
background: #fff;
border: 1px solid var(--s-g200);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgb(0 0 0 / 4%);
}
.acc-card-h {
padding: 12px 16px;
background: var(--s-g50);
border-bottom: 1px solid var(--s-g200);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.acc-card-id {
display: flex;
align-items: center;
gap: 10px;
}
.acc-rsn {
font-family: 'DM Mono', monospace;
font-size: 13px;
font-weight: 700;
color: var(--s-g900);
}
.acc-type-pill {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--s-purple-l);
color: var(--s-purple);
}
.acc-status-pill {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--s-g100);
color: var(--s-g500);
&.act {
background: var(--s-green-l);
color: var(--s-green);
}
}
.acc-card-amt {
display: flex;
align-items: baseline;
gap: 6px;
strong {
font-size: 14px;
color: var(--s-g900);
font-family: 'DM Mono', monospace;
}
}
.acc-card-body {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.acc-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 18px;
> div {
display: flex;
flex-direction: column;
label {
font-size: 10px;
font-weight: 600;
color: var(--s-g500);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1px;
}
span {
font-size: 12px;
color: var(--s-g900);
}
}
}
.acc-pc-block {
background: var(--s-blue-l);
border-radius: 8px;
padding: 12px 14px;
}
.acc-pc-h {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
color: var(--s-blue);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 8px;
}
.acc-pc-addr {
grid-column: 1 / -1;
}
/* Note */
.sft-note {
background: var(--s-amber-l);
border: 1px solid #fed7aa;
border-radius: 10px;
padding: 12px 16px;
display: flex;
gap: 10px;
font-size: 12px;
color: #92400e;
line-height: 1.5;
fa-icon {
font-size: 14px;
flex-shrink: 0;
margin-top: 2px;
}
}
/* Toast */
.sft-toast {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--s-navy);
color: #fff;
padding: 12px 22px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 6px 20px rgb(0 0 0 / 25%);
opacity: 0;
pointer-events: none;
transition: all 0.25s;
z-index: 1500;
&.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@media (width <= 1100px) {
.sft-kpis {
grid-template-columns: repeat(3, 1fr);
}
.sft-codes {
grid-template-columns: 1fr 1fr;
}
.acc-meta {
grid-template-columns: 1fr 1fr;
}
}
@media (width <= 720px) {
.sft-kpis {
grid-template-columns: 1fr 1fr;
}
.sft-codes {
grid-template-columns: 1fr;
}
.acc-meta {
grid-template-columns: 1fr;
}
}
+466
View File
@@ -0,0 +1,466 @@
/**
* 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 } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
SFT_ACCOUNT_RECORDS,
SFT_BANK,
SFT_INT_DIV_RECORDS,
SFT_PERSON_RECORDS,
SftAccountRecord,
SftCode,
SftInterestDividendRecord,
SftPersonRecord
} from './sft-data';
@Component({
selector: 'mifosx-sft',
templateUrl: './sft.component.html',
styleUrls: ['./sft.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
FormsModule
]
})
export class SftComponent {
/** Active tab */
activeTab: 'dashboard' | 'person' | 'account' | 'intdiv' = 'dashboard';
bank = SFT_BANK;
personRecords: SftPersonRecord[] = SFT_PERSON_RECORDS;
accountRecords: SftAccountRecord[] = SFT_ACCOUNT_RECORDS;
intDivRecords: SftInterestDividendRecord[] = SFT_INT_DIV_RECORDS;
/** Person tab — switch which SFT code is being shown */
personSftFilter: 'ALL' | 'SFT-001' | 'SFT-002' | 'SFT-004' = 'ALL';
/** Int/Div tab — switch interest vs dividend */
intDivFilter: 'ALL' | 'SFT-015' | 'SFT-016' = 'ALL';
toastMsg = '';
toastVisible = false;
private toastTimer: any;
showTab(tab: 'dashboard' | 'person' | 'account' | 'intdiv'): void {
this.activeTab = tab;
}
// -------------------- KPIs --------------------
get totalRecords(): number {
return this.personRecords.length + this.accountRecords.length + this.intDivRecords.length;
}
get sft001Count(): number {
return this.personRecords.filter((r) => r.sftCode === 'SFT-001').length;
}
get sft002Count(): number {
return this.personRecords.filter((r) => r.sftCode === 'SFT-002').length;
}
get sft003Count(): number {
return this.accountRecords.length;
}
get sft004Count(): number {
return this.personRecords.filter((r) => r.sftCode === 'SFT-004').length;
}
get sft015Count(): number {
return this.intDivRecords.filter((r) => r.sftCode === 'SFT-015').length;
}
get sft016Count(): number {
return this.intDivRecords.filter((r) => r.sftCode === 'SFT-016').length;
}
get totalReportableAmount(): number {
return (
this.personRecords.reduce((s, r) => s + r.aggGrossReceived, 0) +
this.accountRecords.reduce((s, r) => s + r.aggGrossCashCredited, 0) +
this.intDivRecords.reduce((s, r) => s + r.amount, 0)
);
}
// -------------------- Filters --------------------
get filteredPersons(): SftPersonRecord[] {
if (this.personSftFilter === 'ALL') return this.personRecords;
return this.personRecords.filter((r) => r.sftCode === this.personSftFilter);
}
get filteredIntDiv(): SftInterestDividendRecord[] {
if (this.intDivFilter === 'ALL') return this.intDivRecords;
return this.intDivRecords.filter((r) => r.sftCode === this.intDivFilter);
}
// -------------------- Helpers --------------------
productLabel(t: string): string {
return t.replace(/_/g, ' ');
}
sftLabel(c: SftCode): string {
const map: Record<SftCode, string> = {
'SFT-001': 'Cash Drafts/PO',
'SFT-002': 'Time Deposits',
'SFT-003': 'Cash CA',
'SFT-004': 'Cash SB',
'SFT-015': 'Dividend',
'SFT-016': 'Interest Paid'
};
return map[c];
}
// -------------------- CSV exports --------------------
/** SFT-001/002/004 — Person-wise CSV (comma-separated, all 43 columns) */
exportPersonCsv(filter?: SftCode): void {
const list = filter ? this.personRecords.filter((r) => r.sftCode === filter) : this.filteredPersons;
const headers = [
'Report Serial Number',
'Original Report Serial Number',
'Person Name',
'Person Type',
'Customer Identity',
'Gender',
'Father\u2019s Name',
'PAN',
'Aadhaar Number',
'Form 60 Acknowledgment',
'Identification Type',
'Identification Number',
'Date of birth/Incorporation',
'Nationality/Country of Incorporation',
'AddressType',
'Fix Address',
'City/Town',
'PostCode',
'State',
'CountryCode',
'Business or occupation',
'Primary STD Code',
'Primary Phone No',
'Primary Mobile No',
'Secondary STD Code',
'Secondary Phone No',
'Secondary Mobile No',
'Email',
'Remarks',
'Product Type',
'Aggregate gross amount received from the person',
'Aggregate gross amount received from the person in cash',
'Aggregate gross amount paid to the person',
'TransactionSummry Remarks',
'Product Identifier',
'Last Date of Transaction',
'Amount Received From Person',
'Cash Amount Received From Person',
'Amount Paid To Person',
'Related Account Number',
'Related Institute Name',
'Related Institute Ref Number',
'Financial TransactionDtls Remarks'
];
const rows: string[][] = [headers];
if (list.length === 0) {
rows.push(['NIL Report']);
} else {
for (const r of list) {
rows.push([
r.reportSerialNumber.toString(),
r.originalReportSerialNumber.toString(),
r.personName,
r.personType,
r.customerIdentity,
r.gender,
r.fatherName,
r.pan,
r.aadhaar,
r.form60Ack,
r.identificationType,
r.identificationNumber,
r.dobOrIncorporation.replace(/-/g, '-'),
r.nationality,
r.addressType,
r.fixAddress,
r.city,
r.postCode,
r.state,
r.countryCode,
r.businessOrOccupation,
r.primaryStdCode,
r.primaryPhoneNo,
r.primaryMobileNo,
'',
'',
'',
r.email,
r.remarks,
r.productType,
r.aggGrossReceived.toString(),
r.aggGrossReceivedCash.toString(),
r.aggGrossPaid.toString(),
'',
r.productIdentifier,
r.lastDateOfTransaction,
r.amountReceivedFromPerson.toString(),
r.cashAmountReceivedFromPerson.toString(),
r.amountPaidToPerson.toString(),
r.relatedAccountNumber,
r.relatedInstituteName,
r.relatedInstituteRefNumber,
''
]);
}
}
const fileTag = filter || 'ALL';
this.saveCsv(rows, `${fileTag.replace('-', '_')}.csv`);
this.toast(`${fileTag} exported (${list.length} rows)`);
}
/** SFT-003 — Account-wise CSV with embedded Person Card */
exportAccountCsv(): void {
const headers = [
'Report Serial Number',
'Original Report Serial Number',
'Account Type',
'Account Number',
'Account Holder Name',
'Account Status',
'Branch Reference Number',
'Branch Name',
'Branch Address',
'Branch City/Town',
'Branch Postal Code',
'Branch State',
'Branch Country',
'Branch STD Code',
'Branch Phone Number',
'Branch Mobile Number',
'Branch Fax STD Code',
'Branch Fax Phone No',
'Branch Email',
'Branch Remarks',
'Aggregate gross amount credited to the account in cash',
'Aggregate gross amount debited to the account in cash',
'Amount credited to the account before demonetization',
'Amount credited to the account after demonetization',
'Account Remarks',
'Account Relationship (PC)',
'Person Name (PC)',
'Person Type (PC)',
'Customer Identity (PC)',
'Gender (PC)',
'Father\u2019s Name (PC)',
'PAN (PC)',
'Aadhaar Number (PC)',
'Form 60 Acknowledgement (PC)',
'Identification Type (PC)',
'Identification Number (PC)',
'Date of Birth/ Incorporation (PC)',
'Nationality/Country of Incorporation (PC)',
'Business or Occupation (PC)',
'Address Type (PC-L)',
'Address (PC-L)',
'City/Town (PC-L)',
'Pin Code (PC-L)',
'State (PC-L)',
'Country (PC-L)',
'Primary STD Code (PC)',
'Primary Telephone Number (PC)',
'Primary Mobile Number (PC)',
'Secondary STD Code (PC)',
'Secondary Telephone Number (PC)',
'Secondary Mobile Number (PC)',
'Email (PC)',
'Remarks (PC)'
];
const rows: string[][] = [headers];
if (this.accountRecords.length === 0) {
rows.push(['NIL Report']);
} else {
for (const r of this.accountRecords) {
rows.push([
r.reportSerialNumber.toString(),
r.originalReportSerialNumber.toString(),
r.accountType,
r.accountNumber,
r.accountHolderName,
r.accountStatus,
r.branchReferenceNumber,
r.branchName,
r.branchAddress,
r.branchCity,
r.branchPostalCode,
r.branchState,
r.branchCountry,
r.branchStdCode,
r.branchPhone,
r.branchMobile,
r.branchFaxStd,
r.branchFaxPhone,
r.branchEmail,
'',
r.aggGrossCashCredited.toString(),
r.aggGrossCashDebited.toString(),
r.amountCreditedBeforeDemonetization.toString(),
r.amountCreditedAfterDemonetization.toString(),
'',
r.pcAccountRelationship,
r.pcPersonName,
r.pcPersonType,
r.pcCustomerIdentity,
r.pcGender,
r.pcFatherName,
r.pcPan,
r.pcAadhaar,
r.pcForm60Ack,
r.pcIdentificationType,
r.pcIdentificationNumber,
r.pcDob,
r.pcNationality,
r.pcBusiness,
r.pcAddressType,
r.pcAddress,
r.pcCity,
r.pcPin,
r.pcState,
r.pcCountry,
r.pcPrimaryStd,
r.pcPrimaryPhone,
r.pcPrimaryMobile,
'',
'',
'',
r.pcEmail,
''
]);
}
}
this.saveCsv(rows, 'SFT_003.csv');
this.toast(`SFT-003 exported (${this.accountRecords.length} accounts)`);
}
/** SFT-015 / SFT-016 — Pipe-delimited */
exportIntDivCsv(filter?: 'SFT-015' | 'SFT-016'): void {
const list = filter ? this.intDivRecords.filter((r) => r.sftCode === filter) : this.filteredIntDiv;
const fileTag = filter || 'BOTH';
const lines: string[] = [];
if (filter === 'SFT-015' || (!filter && list.some((r) => r.sftCode === 'SFT-015'))) {
// Dividend file header
const headers15 = [
'RSN',
'FY',
'Name',
'PAN',
'Aadhaar',
'Address',
'Pincode',
'Mobile',
'Email',
'Dividend',
''
];
const div15 = list.filter((r) => r.sftCode === 'SFT-015');
if (filter === 'SFT-015' || div15.length > 0) {
if (filter !== 'SFT-016') {
lines.push(headers15.join('|'));
for (const r of div15) {
lines.push(
[
r.rsn.toString(),
r.fy,
r.name,
r.pan,
r.aadhaar,
r.address || '',
r.pincode || '',
r.mobile,
r.email,
r.amount.toString(),
''
].join('|')
);
}
}
}
}
if (filter === 'SFT-016' || (!filter && list.some((r) => r.sftCode === 'SFT-016'))) {
const headers16 = [
'RSN',
'FY',
'Name',
'PAN',
'Aadhaar',
'Mobile',
'Email',
'Account Number',
'Type of Account',
'Interest',
''
];
const int16 = list.filter((r) => r.sftCode === 'SFT-016');
if (filter !== 'SFT-015' && int16.length > 0) {
if (lines.length > 0) lines.push('');
lines.push(headers16.join('|'));
for (const r of int16) {
lines.push(
[
r.rsn.toString(),
r.fy,
r.name,
r.pan,
r.aadhaar,
r.mobile,
r.email,
r.accountNumber || '',
r.typeOfAccount || '',
r.amount.toString(),
''
].join('|')
);
}
}
}
const filename = filter === 'SFT-015' ? 'SFT15.csv' : filter === 'SFT-016' ? 'SFT16.csv' : 'SFT15_SFT16.csv';
this.saveText(lines.join('\n') + '\n', filename, 'text/csv');
this.toast(`${fileTag} exported (${list.length} rows)`);
}
/** Bundle: all SFT files (one per SFT code) */
exportAll(): void {
setTimeout(() => this.exportPersonCsv('SFT-001'), 0);
setTimeout(() => this.exportPersonCsv('SFT-002'), 200);
setTimeout(() => this.exportAccountCsv(), 400);
setTimeout(() => this.exportPersonCsv('SFT-004'), 600);
setTimeout(() => this.exportIntDivCsv('SFT-015'), 800);
setTimeout(() => this.exportIntDivCsv('SFT-016'), 1000);
}
private saveCsv(rows: string[][], filename: string): void {
const escape = (v: string): string => {
const s = (v || '').replace(/"/g, '""');
if (s.includes(',') || s.includes('"') || s.includes('\n')) return `"${s}"`;
return s;
};
const csv = rows.map((r) => r.map(escape).join(',')).join('\n') + '\n';
this.saveText(csv, filename, 'text/csv');
}
private saveText(content: string, filename: string, mime: string): void {
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) clearTimeout(this.toastTimer);
this.toastTimer = setTimeout(() => (this.toastVisible = false), 2800);
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* 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 { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { SftRoutingModule } from './sft-routing.module';
import { SftComponent } from './sft.component';
@NgModule({
imports: [
SharedModule,
SftRoutingModule,
SftComponent
]
})
export class SftModule {}