diff --git a/src/components/admin/Form16AdminConfig/Form16AdminConfig.tsx b/src/components/admin/Form16AdminConfig/Form16AdminConfig.tsx index bc388e9..e9d1d34 100644 --- a/src/components/admin/Form16AdminConfig/Form16AdminConfig.tsx +++ b/src/components/admin/Form16AdminConfig/Form16AdminConfig.tsx @@ -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'); diff --git a/src/pages/Form16/Form16CreditNoteDetail.tsx b/src/pages/Form16/Form16CreditNoteDetail.tsx index 0b1f611..c5c2c7f 100644 --- a/src/pages/Form16/Form16CreditNoteDetail.tsx +++ b/src/pages/Form16/Form16CreditNoteDetail.tsx @@ -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 | 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 = {}; - 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 { + 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 = {}; - 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 | null>(null); - const [previewCsv, setPreviewCsv] = useState(''); + const [previewDownloadUrl, setPreviewDownloadUrl] = useState(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(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() { - diff --git a/src/pages/Form16/Form16DebitNotes.tsx b/src/pages/Form16/Form16DebitNotes.tsx index db54603..d9c840a 100644 --- a/src/pages/Form16/Form16DebitNotes.tsx +++ b/src/pages/Form16/Form16DebitNotes.tsx @@ -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 | 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 = {}; - 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 { + 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(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() { ) : previewRow ? (
- {/* Block 1 (green): TRNS_UNIQ_NO + CLAIM_NUMBER */} + {/* Block 1 (green): TRNS_UNIQ_NO + TDS_TRNS_ID */}
@@ -207,8 +183,8 @@ export function Form16DebitNotes() {

{previewRow['TRNS_UNIQ_NO'] || '–'}

-

CLAIM_NUMBER

-

{previewRow['CLAIM_NUMBER'] || '–'}

+

TDS_TRNS_ID

+

{previewRow['TDS_TRNS_ID'] || '–'}

diff --git a/src/services/form16Api.ts b/src/services/form16Api.ts index ebb3557..96013c6 100644 --- a/src/services/form16Api.ts +++ b/src/services/form16Api.ts @@ -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 { return url; } +export async function getDebitNoteSapResponse(id: number): Promise { + 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 { + 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 { const { data } = await apiClient.get<{ data?: { creditNote?: Form16CreditNoteItem | null }; creditNote?: Form16CreditNoteItem | null }>(`/form16/requests/${encodeURIComponent(requestId)}/credit-note`);