This commit is contained in:
Abin Toppo
2026-01-28 16:08:19 +05:30
parent 78a74472dd
commit 080f1b57cd
25 changed files with 354 additions and 314 deletions
@@ -1,285 +1,285 @@
import React, { useEffect, useState } from 'react';
import { View, Text, TextInput, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator, Alert, } from 'react-native';
import {
View,
Text,
TextInput,
ScrollView,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
import RNFS from 'react-native-fs';
import * as XLSX from 'xlsx';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { InsideHeader } from '../../../Component/Index';
import colors from '../../../Constant/Color';
import { GET_ENVELOPE_C, SUBMIT_ENVELOPE_C_UPLOAD } from '../../../Services/BBPS/ApiUrls';
import { useNavigation } from '@react-navigation/native';
/* -------------------- UTILS -------------------- */
// Excel column keys
const QTY_KEY = '__EMPTY_1';
const RATE_KEY = '__EMPTY_2';
const VALUE_KEY = '__EMPTY_3';
const TOTAL_LABEL = 'TOTAL';
// Convert ArrayBuffer → Base64
const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return global.btoa(binary);
};
const EnvelopeCExcelPreview = (tenderData: any) => {
const token = useSelector((state: any) => state.token.token);
const navigation = useNavigation() as any;
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
console.log("kllkl",tenderData)
useEffect(() => {
loadExcelFromApi();
}, []);
// Find column dynamically by keywords
const findColumnKey = (columns: string[], keywords: string[]) =>
columns.find(col =>
keywords.some(k => col.toLowerCase().includes(k.toLowerCase()))
);
// ArrayBuffer → Base64
const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
// Detect TOTAL row (any column contains TOTAL)
const isTotalRow = (row: any) =>
Object.values(row).some(v => String(v).toUpperCase() === 'TOTAL');
/* -------------------- COMPONENT -------------------- */
const EnvelopeCExcelPreview = (props: any) => {
const tenderData = props;
const token = useSelector((state: any) => state.token.token);
const navigation = useNavigation() as any;
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadExcelFromApi();
}, []);
/* -------------------- LOAD EXCEL -------------------- */
const loadExcelFromApi = async () => {
try {
setLoading(true);
const response = await axios.get(
`${GET_ENVELOPE_C}${tenderData?.tenderId?.tenderId}`,
{
responseType: 'arraybuffer',
headers: {
Authentication: `Bearer ${token.token}`,
},
}
return global.btoa(binary);
};
);
// Calculate TOTAL VALUE
const calculateTotal = (rows: any[]) =>
rows.reduce((sum, row) => {
if (typeof row['DEMO DEPARTEMENT'] !== 'number') return sum;
const val = Number(row[VALUE_KEY]);
return sum + (isNaN(val) ? 0 : val);
}, 0);
const base64 = arrayBufferToBase64(response.data);
const workbook = XLSX.read(base64, { type: 'base64' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' });
// LOAD EXCEL
const loadExcelFromApi = async () => {
try {
setLoading(true);
setColumns(Object.keys(rows[0] || {}));
setData(rows);
const response = await axios.get(`${GET_ENVELOPE_C}${tenderData?.tenderId?.tenderId}`, {
responseType: 'arraybuffer',
headers: {
Authentication: `Bearer ${token.token}`,
},
});
console.log("excel response",response)
} catch (e) {
Alert.alert('Error', 'Failed to load Excel');
} finally {
setLoading(false);
}
};
const base64 = arrayBufferToBase64(response.data);
const workbook = XLSX.read(base64, { type: 'base64' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: '' });
/* -------------------- UPDATE CELL -------------------- */
setColumns(Object.keys(rows[0] || {}));
setData(rows);
const updateCell = (rowIndex: number, key: string, value: string) => {
const updated = [...data];
const row = updated[rowIndex];
await RNFS.writeFile(
`${RNFS.DocumentDirectoryPath}/envelopeC.xlsx`,
base64,
'base64'
);
} catch (e) {
Alert.alert('Error', 'Failed to load Excel');
} finally {
setLoading(false);
}
};
if (isTotalRow(row)) return;
// UPDATE CELL (QTY / RATE) + AUTO VALUE + AUTO TOTAL
const updateCell = (rowIndex: number, key: string, value: string) => {
const updated = [...data];
const row = updated[rowIndex];
const qtyKey:any = findColumnKey(columns, ['qty', 'quantity']);
const rateKey:any = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
// TOTAL row locked
if (row['DEMO DEPARTEMENT'] === TOTAL_LABEL) return;
if (key === qtyKey || key === rateKey) {
row[key] = value;
// Allow only QTY & RATE edit
if (key === QTY_KEY || key === RATE_KEY) {
row[key] = value;
const qty = Number(row[qtyKey]);
const rate = Number(row[rateKey]);
const qty = Number(row[QTY_KEY]);
const rate = Number(row[RATE_KEY]);
// 🔥 Auto VALUE = QTY × RATE
row[VALUE_KEY] =
!isNaN(qty) && !isNaN(rate) ? qty * rate : 0;
}
// 🔥 Update TOTAL
const totalRowIndex = updated.findIndex(
r => r['DEMO DEPARTEMENT'] === TOTAL_LABEL
);
if (totalRowIndex !== -1) {
updated[totalRowIndex][VALUE_KEY] = calculateTotal(updated);
}
setData(updated);
};
// SAVE UPDATED EXCEL
const saveUpdatedExcel = async () => {
try {
setLoading(true);
const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// Generate binary string
const binary = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'binary',
});
// Convert binary string to buffer
const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
buffer[i] = binary.charCodeAt(i) & 0xff;
}
const filePath = `${RNFS.DocumentDirectoryPath}/envelopeC_updated.xlsx`;
// Write as binary
await RNFS.writeFile(filePath, buffer.toString(), 'ascii');
// const fileBinary = await RNFS.readFile(filePath, 'ascii');
// console.log('Excel binary content:', fileBinary);
// Alert.alert('Success', 'Excel saved successfully');
const formData = new FormData();
// requestDto payload
formData.append(
'requestDto',
JSON.stringify({
tenderId: tenderData?.tenderId?.tenderId,
deptId: tenderData?.tenderId?.deptId,
typesOfTender: tenderData?.tenderId?.typesOfTender,
fieldValue: '',
fieldValueInWord: 'zero',
})
);
// binary file
formData.append('file', {
uri: `file://${filePath}`,
name: 'envelopeC_updated.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const res = await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authentication: `Bearer ${token.token}`,
},
});
navigation.goBack();
console.log("res of updated excel file : ", res)
} catch (e) {
console.error(e);
Alert.alert('Error', 'Save failed');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<View style={styles.loader}>
<ActivityIndicator size="large" />
</View>
);
if (valueKey) {
row[valueKey] =
!isNaN(qty) && !isNaN(rate) ? qty * rate : 0;
}
}
// Recalculate TOTAL
const totalRowIndex = updated.findIndex(isTotalRow);
if (totalRowIndex !== -1 && valueKey) {
updated[totalRowIndex][valueKey] = updated.reduce((sum, r) => {
if (isTotalRow(r)) return sum;
const val = Number(r[valueKey]);
return sum + (isNaN(val) ? 0 : val);
}, 0);
}
setData(updated);
};
/* -------------------- SAVE & UPLOAD EXCEL -------------------- */
const saveUpdatedExcel = async () => {
try {
setLoading(true);
const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const binary = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'binary',
});
const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
buffer[i] = binary.charCodeAt(i) & 0xff;
}
const filePath = `${RNFS.DocumentDirectoryPath}/envelopeC_updated.xlsx`;
await RNFS.writeFile(filePath, buffer.toString(), 'ascii');
const formData = new FormData();
formData.append(
'requestDto',
JSON.stringify({
tenderId: tenderData?.tenderId?.tenderId,
deptId: tenderData?.tenderId?.deptId,
typesOfTender: tenderData?.tenderId?.typesOfTender,
fieldValue: '',
fieldValueInWord: 'zero',
})
);
formData.append('file', {
uri: `file://${filePath}`,
name: 'envelopeC_updated.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authentication: `Bearer ${token.token}`,
},
});
navigation.goBack();
} catch (e) {
Alert.alert('Error', 'Save failed');
} finally {
setLoading(false);
}
};
/* -------------------- UI -------------------- */
if (loading) {
return (
<View style={styles.container}>
<ScrollView horizontal contentContainerStyle={{ paddingHorizontal: 10, paddingTop: 20, backgroundColor: colors.white }}>
<View>
{/* HEADER */}
<View style={styles.row}>
{columns.map(col => (
<Text key={col} style={styles.headerCell}>
{col}
</Text>
))}
</View>
{/* DATA */}
<ScrollView style={{ maxHeight: 420 }}>
{data.map((row, rowIndex) => (
<View key={rowIndex} style={styles.row}>
{columns.map(key => {
const isTotal = row['DEMO DEPARTEMENT'] === TOTAL_LABEL;
const isEditable =
!isTotal && (key === QTY_KEY || key === RATE_KEY);
return (
<TextInput
key={key}
value={String(row[key])}
editable={isEditable}
keyboardType={
key === QTY_KEY || key === RATE_KEY
? 'numeric'
: 'default'
}
style={[
styles.cell,
isTotal && styles.totalCell,
key === VALUE_KEY && styles.valueCell,
]}
onChangeText={text =>
updateCell(rowIndex, key, text)
}
/>
);
})}
</View>
))}
</ScrollView>
</View>
</ScrollView>
{tenderData?.buttonEnable && <TouchableOpacity style={styles.button} onPress={saveUpdatedExcel}>
<Text style={styles.buttonText}>Save Updated Excel</Text>
</TouchableOpacity>}
</View>
<View style={styles.loader}>
<ActivityIndicator size="large" />
</View>
);
}
const qtyKey = findColumnKey(columns, ['qty', 'quantity']);
const rateKey = findColumnKey(columns, ['rate', 'price']);
const valueKey = findColumnKey(columns, ['value', 'amount', 'total']);
return (
<View style={styles.container}>
<ScrollView horizontal contentContainerStyle={{ padding: 10 }}>
<View>
{/* HEADER */}
<View style={styles.row}>
{columns.map(col => (
<Text key={col} style={styles.headerCell}>{col}</Text>
))}
</View>
{/* DATA */}
<ScrollView style={{ maxHeight: 420 }}>
{data.map((row, rowIndex) => (
<View key={rowIndex} style={styles.row}>
{columns.map(key => {
const editable =
!isTotalRow(row) && (key === qtyKey || key === rateKey);
return (
<TextInput
key={key}
value={String(row[key])}
editable={editable}
keyboardType={editable ? 'numeric' : 'default'}
style={[
styles.cell,
isTotalRow(row) && styles.totalCell,
key === valueKey && styles.valueCell,
]}
onChangeText={text =>
updateCell(rowIndex, key, text)
}
/>
);
})}
</View>
))}
</ScrollView>
</View>
</ScrollView>
{tenderData?.buttonEnable && (
<TouchableOpacity style={styles.button} onPress={saveUpdatedExcel}>
<Text style={styles.buttonText}>Save Updated Excel</Text>
</TouchableOpacity>
)}
</View>
);
};
export default EnvelopeCExcelPreview;
/* -------------------- STYLES -------------------- */
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
loader: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 18, fontWeight: '700', marginBottom: 10 },
row: { flexDirection: 'row' },
headerCell: {
minWidth: 140,
padding: 10,
backgroundColor: '#f1f5f9',
borderWidth: 1,
fontWeight: '700',
},
cell: {
minWidth: 140,
padding: 10,
borderWidth: 1,
borderColor: '#cbd5e1',
},
valueCell: {
backgroundColor: '#f8fafc',
},
totalCell: {
backgroundColor: '#e5e7eb',
fontWeight: '700',
},
button: {
marginTop: 15,
backgroundColor: colors.primary,
padding: 14,
borderRadius: 8,
},
buttonText: {
color: '#fff',
textAlign: 'center',
fontWeight: '600',
},
container: { flex: 1, backgroundColor: '#fff' },
loader: { flex: 1, justifyContent: 'center', alignItems: 'center' },
row: { flexDirection: 'row' },
headerCell: {
minWidth: 140,
padding: 10,
backgroundColor: '#f1f5f9',
borderWidth: 1,
fontWeight: '700',
},
cell: {
minWidth: 140,
padding: 10,
borderWidth: 1,
borderColor: '#cbd5e1',
},
valueCell: {
backgroundColor: '#f8fafc',
},
totalCell: {
backgroundColor: '#e5e7eb',
fontWeight: '700',
},
button: {
margin: 15,
backgroundColor: colors.primary,
padding: 14,
borderRadius: 8,
},
buttonText: {
color: '#fff',
textAlign: 'center',
fontWeight: '600',
},
});
+3 -1
View File
@@ -17,7 +17,7 @@ const LiveTender = () => {
console.log("userdata",user)
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseTender);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch }: any = useFetchLiveTender();
console.log("to show ",data)
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
useEffect(() => {
@@ -49,6 +49,8 @@ const LiveTender = () => {
const renderItem = useCallback(
({ item, index }: any) => {
console.log("opopopopoopopopop",item.tenderNumber,item)
if(item.tenderStatus==="Cancelled") return null;
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.tenderId === item.tenderId)
return <LiveTenderTile
contractorId={user?.id}
+10 -7
View File
@@ -52,6 +52,7 @@ const LiveTenderDetail = () => {
const tender = route?.params;
console.log("object---",tender)
const { data } = useFetchLiveTenderById(tender?.tenderId);
console.log("huh",data)
const [downloadingId, setDownloadingId] = useState<string | number | null>(null);
const insets = useSafeAreaInsets()
if (!data?.data?.[0]) return null;
@@ -198,38 +199,40 @@ const LiveTenderDetail = () => {
<InfoRow
label="Purchase Start"
value={formatDateTime(
tenderData?.keydates?.tenderPurchaseDate?.startDateTime,
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.startDateTime : tenderData?.keydates?.tenderPurchaseDate?.startDateTime
)}
/>
<InfoRow
label="Purchase End"
value={formatDateTime(
tenderData?.keydates?.tenderPurchaseDate?.endDateTime,
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.tenderPurchaseDate?.endDateTime : tenderData?.keydates?.tenderPurchaseDate?.endDateTime
)}
/>
<InfoRow
label="Bid Submission Start"
value={formatDateTime(
tenderData?.keydates?.bidSubmissionDate?.startDateTime,
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.startDateTime : tenderData?.keydates?.bidSubmissionDate?.startDateTime
)}
/>
<InfoRow
label="Bid Submission End"
value={formatDateTime(
tenderData?.keydates?.bidSubmissionDate?.endDateTime,
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.bidSubmissionDate?.endDateTime : tenderData?.keydates?.bidSubmissionDate?.endDateTime,
)}
/>
<InfoRow
label="Opening Envelope A"
value={formatDateTime(tenderData?.keydates?.openingEA)}
value={formatDateTime(
tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEA : tenderData?.keydates?.openingEA
)}
/>
<InfoRow
label="Opening Envelope B"
value={formatDateTime(tenderData?.keydates?.openingEB)}
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEB : tenderData?.keydates?.openingEB)}
/>
<InfoRow
label="Opening Envelope C"
value={formatDateTime(tenderData?.keydates?.openingEC)}
value={formatDateTime(tender.tenderStatus === "Amended" ? tenderData?.tenderKeyDatesAmendment?.openingEC : tenderData?.keydates?.openingEC)}
/>
</View>
</Section>
+7 -4
View File
@@ -21,7 +21,7 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
tenderNumber,
}) => {
const navigation: any = useNavigation();
const purchaseEnded = !moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss'));
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss'));
const contractorIds = typeof data?.contractorId === 'string'
? data.contractorId.split(',').map((id: any) => id.trim())
@@ -30,7 +30,10 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
: [];
const purchased = contractorIds.includes(String(contractorId));
console.log("item is ",data)
if(data.tenderStatus==="Amended"){
}
return (
<View style={styles.cardWrapper}>
@@ -77,7 +80,7 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Deadline</Text>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}>
{moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY')}
{moment(purchaseEnd, 'YYYY-MM-DD HH:mm:ss').format('DD MMM, YY')}
</Text>
</View>
</View>
@@ -108,7 +111,7 @@ const LiveTenderTile: React.FC<LiveTenderTileProps> = ({
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
]}
>
{purchaseEnded ? 'Expired' : (purchased ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart')}
{purchaseEnded ? (purchased ? 'Purchased' : 'Expired') : (purchased ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart')}
</Text>
{(!purchaseEnded && !purchased) && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />