Compare commits

..

2 Commits

Author SHA1 Message Date
Aaditya Jaiswal
6d8a60581d Merge branch 'laxman_dev' updates
Made-with: Cursor
2026-03-24 11:12:39 +05:30
Aaditya Jaiswal
3a6a8c2472 Fixed claim number issue at debit note and SAP responses from database 2026-03-24 11:04:49 +05:30
5 changed files with 92 additions and 160 deletions

View File

@ -132,12 +132,12 @@ export function Form16AdminConfig() {
const [alertSubmitForm16FrequencyDays, setAlertSubmitForm16FrequencyDays] = useState(0);
const [alertSubmitForm16FrequencyHours, setAlertSubmitForm16FrequencyHours] = useState(24);
const [alertSubmitForm16RunAtTime, setAlertSubmitForm16RunAtTime] = useState('09:00');
const [alertSubmitForm16Template, setAlertSubmitForm16Template] = useState('Please submit your Form 16 at your earliest. [Name], due date: [DueDate].');
const [alertSubmitForm16Template, setAlertSubmitForm16Template] = useState('Dear [Name], please submit Form 16A for the pending period. Due: [DueDate].');
const [reminderNotificationEnabled, setReminderNotificationEnabled] = useState(true);
const [reminderFrequencyDays, setReminderFrequencyDays] = useState(0);
const [reminderFrequencyHours, setReminderFrequencyHours] = useState(12);
const [reminderRunAtTime, setReminderRunAtTime] = useState('10:00');
const [reminderNotificationTemplate, setReminderNotificationTemplate] = useState('Reminder: Form 16 submission is pending. [Name], [Request ID]. Please review.');
const [reminderNotificationTemplate, setReminderNotificationTemplate] = useState('Reminder: Dear [Name], your Form 16A submission is pending for request [Request ID]. Please complete it.');
useEffect(() => {
let mounted = true;
@ -166,12 +166,12 @@ export function Form16AdminConfig() {
setAlertSubmitForm16FrequencyDays(config.alertSubmitForm16FrequencyDays ?? 0);
setAlertSubmitForm16FrequencyHours(config.alertSubmitForm16FrequencyHours ?? 24);
setAlertSubmitForm16RunAtTime(config.alertSubmitForm16RunAtTime !== undefined && config.alertSubmitForm16RunAtTime !== null ? config.alertSubmitForm16RunAtTime : '09:00');
setAlertSubmitForm16Template(config.alertSubmitForm16Template ?? 'Please submit your Form 16 at your earliest. [Name], due date: [DueDate].');
setAlertSubmitForm16Template(config.alertSubmitForm16Template ?? 'Dear [Name], please submit Form 16A for the pending period. Due: [DueDate].');
setReminderNotificationEnabled(config.reminderNotificationEnabled ?? true);
setReminderFrequencyDays(config.reminderFrequencyDays ?? 0);
setReminderFrequencyHours(config.reminderFrequencyHours ?? 12);
setReminderRunAtTime(config.reminderRunAtTime !== undefined && config.reminderRunAtTime !== null ? config.reminderRunAtTime : '10:00');
setReminderNotificationTemplate(config.reminderNotificationTemplate ?? 'Reminder: Form 16 submission is pending. [Name], [Request ID]. Please review.');
setReminderNotificationTemplate(config.reminderNotificationTemplate ?? 'Reminder: Dear [Name], your Form 16A submission is pending for request [Request ID]. Please complete it.');
})
.catch(() => {
if (mounted) toast.error('Failed to load Form 16 configuration');

View File

@ -6,9 +6,8 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ArrowLeft, Mail, Download, Loader2, RefreshCw, FileText, IndianRupee, CalendarClock } from 'lucide-react';
import { getCreditNoteById, getCreditNoteDownloadUrl, type CreditNoteDetailResponse } from '@/services/form16Api';
import { getCreditNoteById, getCreditNoteSapResponse, type CreditNoteDetailResponse, type Form16SapResponseRecord } from '@/services/form16Api';
import { toast } from 'sonner';
import apiClient from '@/services/authApi';
function formatDate(value: string | null | undefined): string {
if (!value) return '';
@ -25,41 +24,14 @@ function formatAmount(value: number | null | undefined): string {
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(value);
}
function buildAbsoluteBackendUrl(url: string): string {
if (!url) return url;
if (/^https?:\/\//i.test(url)) return url;
const apiBase = ((import.meta as any).env?.VITE_API_BASE_URL as string | undefined) || 'http://localhost:5000/api/v1';
const origin = apiBase.replace(/\/api\/v1\/?$/i, '');
return `${origin}${url.startsWith('/') ? '' : '/'}${url}`;
}
function parseSapResponseCsv(rawCsv: string): Record<string, string> | null {
const lines = rawCsv
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
if (lines.length < 2) return null;
const header = lines[0]!.split('|').map((h) => h.trim());
const isUsefulRow = (values: string[]) => {
const obj: Record<string, string> = {};
header.forEach((h, idx) => (obj[h] = (values[idx] || '').trim()));
const trns = (obj.TRNS_UNIQ_NO || '').trim();
const docNo = (obj.DOC_NO || '').trim();
const msgTyp = (obj.MSG_TYP || '').trim();
const tdsId = (obj.TDS_TRNS_ID || '').trim();
if (trns) return true;
if (tdsId && (docNo || msgTyp) && tdsId.toUpperCase() !== 'MSG_TYP' && tdsId.toUpperCase() !== 'MESSAGE') return true;
return false;
function mapSapResponseToPreviewRow(sap: Form16SapResponseRecord): Record<string, string> {
return {
TRNS_UNIQ_NO: sap.trnsUniqNo || '',
TDS_TRNS_ID: sap.tdsTransId || sap.claimNumber || '',
DOC_NO: sap.sapDocumentNumber || '',
MSG_TYP: sap.msgTyp || '',
MESSAGE: sap.message || '',
};
for (let i = lines.length - 1; i >= 1; i--) {
const values = lines[i]!.split('|');
if (isUsefulRow(values)) {
const obj: Record<string, string> = {};
header.forEach((h, idx) => (obj[h] = (values[idx] || '').trim()));
return obj;
}
}
return null;
}
function formatIssuedAt(value: string | null | undefined): string {
@ -83,7 +55,7 @@ export function Form16CreditNoteDetail() {
const [previewOpen, setPreviewOpen] = useState(false);
const [previewLoading, setPreviewLoading] = useState(false);
const [previewRow, setPreviewRow] = useState<Record<string, string> | null>(null);
const [previewCsv, setPreviewCsv] = useState<string>('');
const [previewDownloadUrl, setPreviewDownloadUrl] = useState<string | null>(null);
useEffect(() => {
const numId = id ? parseInt(id, 10) : NaN;
@ -134,18 +106,14 @@ export function Form16CreditNoteDetail() {
const numId = id ? parseInt(id, 10) : NaN;
if (Number.isNaN(numId)) return;
try {
const url = await getCreditNoteDownloadUrl(numId);
setPreviewOpen(true);
setPreviewLoading(true);
setPreviewRow(null);
setPreviewCsv('');
const absUrl = buildAbsoluteBackendUrl(url);
const res = await apiClient.get<Blob>(absUrl, { responseType: 'blob' });
const rawCsv = await res.data.text();
const row = parseSapResponseCsv(rawCsv);
if (!row) throw new Error('Could not parse SAP response CSV');
setPreviewDownloadUrl(null);
const payload = await getCreditNoteSapResponse(numId);
const row = mapSapResponseToPreviewRow(payload.sapResponse);
setPreviewRow(row);
setPreviewCsv(rawCsv);
setPreviewDownloadUrl(payload.url || null);
} catch (e: any) {
const msg =
e?.response?.status === 409
@ -184,7 +152,7 @@ export function Form16CreditNoteDetail() {
setPreviewOpen(open);
if (!open) {
setPreviewRow(null);
setPreviewCsv('');
setPreviewDownloadUrl(null);
}
}}
>
@ -268,19 +236,13 @@ export function Form16CreditNoteDetail() {
</Button>
<Button
onClick={() => {
if (!previewCsv) return;
const fileName = `${creditNote.creditNoteNumber || `credit-note-${id}`}.csv`;
const blob = new Blob([previewCsv], { type: 'text/csv;charset=utf-8' });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(href);
if (!previewDownloadUrl) {
toast.error('CSV download link not available');
return;
}
window.open(previewDownloadUrl, '_blank');
}}
disabled={!previewRow || previewLoading}
disabled={!previewRow || previewLoading || !previewDownloadUrl}
>
<Download className="w-4 h-4 mr-2" />
Download CSV

View File

@ -8,8 +8,9 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Search, Loader2, Receipt, Download, RefreshCw, FileText, IndianRupee, CalendarClock } from 'lucide-react';
import {
listCreditNotes,
getCreditNoteDownloadUrl,
getCreditNoteSapResponse,
type Form16CreditNoteItem,
type Form16SapResponseRecord,
type ListCreditNotesParams,
type ListCreditNotesSummary,
} from '@/services/form16Api';
@ -38,9 +39,8 @@ const DEFAULT_SUMMARY: ListCreditNotesSummary = {
};
type SapResponsePreview = {
fileUrl: string;
fileName: string;
rawCsv: string;
downloadUrl?: string | null;
row: Record<string, string>;
meta: {
amountText: string;
@ -48,43 +48,14 @@ type SapResponsePreview = {
};
};
function buildAbsoluteBackendUrl(url: string): string {
if (!url) return url;
if (/^https?:\/\//i.test(url)) return url;
const apiBase = ((import.meta as any).env?.VITE_API_BASE_URL as string | undefined) || 'http://localhost:5000/api/v1';
const origin = apiBase.replace(/\/api\/v1\/?$/i, '');
return `${origin}${url.startsWith('/') ? '' : '/'}${url}`;
}
function parseSapResponseCsv(rawCsv: string): Record<string, string> | null {
const lines = rawCsv
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
if (lines.length < 2) return null;
const header = lines[0]!.split('|').map((h) => h.trim());
const isUsefulRow = (values: string[]) => {
const obj: Record<string, string> = {};
header.forEach((h, idx) => (obj[h] = (values[idx] || '').trim()));
const trns = (obj.TRNS_UNIQ_NO || '').trim();
const docNo = (obj.DOC_NO || '').trim();
const msgTyp = (obj.MSG_TYP || '').trim();
const tdsId = (obj.TDS_TRNS_ID || '').trim();
if (trns) return true;
if (tdsId && (docNo || msgTyp) && tdsId.toUpperCase() !== 'MSG_TYP' && tdsId.toUpperCase() !== 'MESSAGE') return true;
return false;
function mapSapResponseToPreviewRow(sap: Form16SapResponseRecord): Record<string, string> {
return {
TRNS_UNIQ_NO: sap.trnsUniqNo || '',
TDS_TRNS_ID: sap.tdsTransId || sap.claimNumber || '',
DOC_NO: sap.sapDocumentNumber || '',
MSG_TYP: sap.msgTyp || '',
MESSAGE: sap.message || '',
};
for (let i = lines.length - 1; i >= 1; i--) {
const values = lines[i]!.split('|');
if (isUsefulRow(values)) {
const obj: Record<string, string> = {};
header.forEach((h, idx) => (obj[h] = (values[idx] || '').trim()));
return obj;
}
}
return null;
}
function formatIssuedAt(value: string | null | undefined): string {
@ -133,22 +104,16 @@ export function Form16CreditNotes() {
setPreviewLoading(true);
setPreview(null);
try {
const url = await getCreditNoteDownloadUrl(noteId);
const absUrl = buildAbsoluteBackendUrl(url);
const payload = await getCreditNoteSapResponse(noteId);
const fileName = (creditNoteNumber || `credit-note-${noteId}`).trim() + '.csv';
const res = await apiClient.get<Blob>(absUrl, { responseType: 'blob' });
const blob = res.data;
const rawCsv = await blob.text();
const row = parseSapResponseCsv(rawCsv);
if (!row) throw new Error('Could not parse SAP response CSV');
const row = mapSapResponseToPreviewRow(payload.sapResponse);
// Enrich with UI meta (amount/date from listing row when available)
const note = creditNotes.find((n) => Number(n.id) === Number(noteId));
const amountText = formatAmount(note?.amount ?? null);
const issuedAtText = formatIssuedAt(note?.issueDate ?? null);
setPreview({ fileUrl: absUrl, fileName, rawCsv, row, meta: { amountText, issuedAtText } });
setPreview({ fileName, row, meta: { amountText, issuedAtText }, downloadUrl: payload.url || null });
} catch (e: any) {
const msg =
e?.response?.status === 409
@ -162,16 +127,11 @@ export function Form16CreditNotes() {
}, [creditNotes]);
const downloadPreviewCsv = useCallback(async () => {
if (!preview) return;
const blob = new Blob([preview.rawCsv], { type: 'text/csv;charset=utf-8' });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = preview.fileName;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(href);
if (!preview?.downloadUrl) {
toast.error('CSV download link not available');
return;
}
window.open(preview.downloadUrl, '_blank');
}, [preview]);
const filteredNotes = useMemo(() => {
@ -273,7 +233,7 @@ export function Form16CreditNotes() {
<Button variant="outline" onClick={() => setPreviewOpen(false)}>
Close
</Button>
<Button onClick={downloadPreviewCsv} disabled={!preview || previewLoading}>
<Button onClick={downloadPreviewCsv} disabled={!preview || previewLoading || !preview?.downloadUrl}>
<Download className="w-4 h-4 mr-2" />
Download CSV
</Button>

View File

@ -9,8 +9,9 @@ import { Search, Loader2, Receipt, RefreshCw, Eye, FileText, IndianRupee, Calend
import { toast } from 'sonner';
import {
listDebitNotes,
getDebitNoteSapResponseUrl,
getDebitNoteSapResponse,
type Form16DebitNoteListItem,
type Form16SapResponseRecord,
type ListDebitNotesParams,
type ListDebitNotesSummary,
} from '@/services/form16Api';
@ -43,34 +44,14 @@ function formatIssuedAt(value: string | null | undefined): string {
}
}
function buildAbsoluteBackendUrl(url: string): string {
if (!url) return url;
if (/^https?:\/\//i.test(url)) return url;
const apiBase = ((import.meta as any).env?.VITE_API_BASE_URL as string | undefined) || 'http://localhost:5000/api/v1';
const origin = apiBase.replace(/\/api\/v1\/?$/i, '');
return `${origin}${url.startsWith('/') ? '' : '/'}${url}`;
}
function parseSapResponseCsv(rawCsv: string): Record<string, string> | null {
const lines = rawCsv
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
if (lines.length < 2) return null;
const header = lines[0]!.split('|').map((h) => h.trim());
// Debit response should contain: TRNS_UNIQ_NO, CLAIM_NUMBER, DOC_NO, MSG_TYP, MESSAGE
for (let i = lines.length - 1; i >= 1; i--) {
const values = lines[i]!.split('|');
const obj: Record<string, string> = {};
header.forEach((h, idx) => (obj[h] = (values[idx] || '').trim()));
const trns = (obj.TRNS_UNIQ_NO || '').trim();
const claim = (obj.CLAIM_NUMBER || '').trim();
const doc = (obj.DOC_NO || '').trim();
const typ = (obj.MSG_TYP || '').trim();
if (trns || (claim && (doc || typ))) return obj;
}
return null;
function mapSapResponseToPreviewRow(sap: Form16SapResponseRecord): Record<string, string> {
return {
TRNS_UNIQ_NO: sap.trnsUniqNo || '',
TDS_TRNS_ID: sap.tdsTransId || sap.claimNumber || '',
DOC_NO: sap.sapDocumentNumber || '',
MSG_TYP: sap.msgTyp || '',
MESSAGE: sap.message || '',
};
}
const DEFAULT_SUMMARY: ListDebitNotesSummary = {
@ -129,13 +110,8 @@ export function Form16DebitNotes() {
setPreviewRow(null);
setPreviewMeta({ amountText: formatAmount(note.amount), issuedAtText: formatIssuedAt(note.issueDate) });
try {
const url = await getDebitNoteSapResponseUrl(Number(note.id));
const absUrl = buildAbsoluteBackendUrl(url);
const res = await apiClient.get<Blob>(absUrl, { responseType: 'blob' });
const rawCsv = await res.data.text();
const row = parseSapResponseCsv(rawCsv);
if (!row) throw new Error('Could not parse SAP response CSV');
setPreviewRow(row);
const payload = await getDebitNoteSapResponse(Number(note.id));
setPreviewRow(mapSapResponseToPreviewRow(payload.sapResponse));
} catch (e: any) {
const msg =
e?.response?.status === 409
@ -195,7 +171,7 @@ export function Form16DebitNotes() {
</div>
) : previewRow ? (
<div className="space-y-4">
{/* Block 1 (green): TRNS_UNIQ_NO + CLAIM_NUMBER */}
{/* Block 1 (green): TRNS_UNIQ_NO + TDS_TRNS_ID */}
<div className="rounded-xl border border-emerald-200 bg-emerald-50/70 p-4">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-emerald-700" />
@ -207,8 +183,8 @@ export function Form16DebitNotes() {
<p className="text-sm font-semibold text-emerald-900 break-all mt-1">{previewRow['TRNS_UNIQ_NO'] || ''}</p>
</div>
<div className="rounded-lg border border-emerald-200 bg-white/70 p-3">
<p className="text-xs text-emerald-700 uppercase tracking-wide">CLAIM_NUMBER</p>
<p className="text-sm font-semibold text-emerald-900 break-all mt-1">{previewRow['CLAIM_NUMBER'] || ''}</p>
<p className="text-xs text-emerald-700 uppercase tracking-wide">TDS_TRNS_ID</p>
<p className="text-sm font-semibold text-emerald-900 break-all mt-1">{previewRow['TDS_TRNS_ID'] || ''}</p>
</div>
</div>
</div>

View File

@ -124,6 +124,26 @@ export interface ListDebitNotesParams {
quarter?: string;
}
export interface Form16SapResponseRecord {
fileName: string | null;
trnsUniqNo: string | null;
tdsTransId: string | null;
claimNumber: string | null;
sapDocumentNumber: string | null;
msgTyp: string | null;
message: string | null;
docDate: string | null;
tdsAmt: string | null;
storageUrl: string | null;
createdAt: string | null;
updatedAt: string | null;
}
export interface Form16SapResponsePayload {
sapResponse: Form16SapResponseRecord;
url?: string | null;
}
export interface Form16Permissions {
canViewForm16Submission: boolean;
canView26AS: boolean;
@ -186,6 +206,20 @@ export async function getDebitNoteSapResponseUrl(id: number): Promise<string> {
return url;
}
export async function getDebitNoteSapResponse(id: number): Promise<Form16SapResponsePayload> {
const { data } = await apiClient.get<{ data?: Form16SapResponsePayload } | Form16SapResponsePayload>(`/form16/debit-notes/${id}/sap-response`);
const payload = (data && typeof data === 'object' && 'data' in data ? data.data : data) as Form16SapResponsePayload | undefined;
if (!payload?.sapResponse) throw new Error('SAP response not available');
return payload;
}
export async function getCreditNoteSapResponse(id: number): Promise<Form16SapResponsePayload> {
const { data } = await apiClient.get<{ data?: Form16SapResponsePayload } | Form16SapResponsePayload>(`/form16/credit-notes/${id}/sap-response`);
const payload = (data && typeof data === 'object' && 'data' in data ? data.data : data) as Form16SapResponsePayload | undefined;
if (!payload?.sapResponse) throw new Error('SAP response not available');
return payload;
}
/** Get credit note linked to a Form 16 request (for workflow tab). */
export async function getCreditNoteByRequestId(requestId: string): Promise<Form16CreditNoteItem | null> {
const { data } = await apiClient.get<{ data?: { creditNote?: Form16CreditNoteItem | null }; creditNote?: Form16CreditNoteItem | null }>(`/form16/requests/${encodeURIComponent(requestId)}/credit-note`);