diff --git a/package-lock.json b/package-lock.json index 91c64a1..613ff0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-native-app-intro-slider": "^4.0.4", "react-native-bouncy-checkbox": "^4.1.3", "react-native-date-picker": "^5.0.13", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.30.0", "react-native-gifted-charts": "^1.4.70", "react-native-haptic-feedback": "^2.3.3", @@ -50,6 +51,7 @@ "redux-persist": "^6.0.0", "sockjs-client": "^1.6.1", "text-encoding": "^0.7.0", + "xlsx": "^0.18.5", "yup": "^1.7.1" }, "devDependencies": { @@ -4568,6 +4570,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -5094,6 +5105,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5406,6 +5422,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5571,6 +5600,15 @@ "node": ">= 0.12.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", @@ -5797,6 +5835,18 @@ } } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -7486,6 +7536,15 @@ "node": ">=0.10.0" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -11751,6 +11810,25 @@ "react-native": ">= 0.64.3" } }, + "node_modules/react-native-fs": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", + "integrity": "sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==", + "license": "MIT", + "dependencies": { + "base-64": "^0.1.0", + "utf8": "^3.0.0" + }, + "peerDependencies": { + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, "node_modules/react-native-gesture-handler": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz", @@ -12902,6 +12980,18 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -13741,6 +13831,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13949,6 +14045,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14003,6 +14117,27 @@ "async-limiter": "~1.0.0" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 3c534f4..d1e932d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react-native-app-intro-slider": "^4.0.4", "react-native-bouncy-checkbox": "^4.1.3", "react-native-date-picker": "^5.0.13", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.30.0", "react-native-gifted-charts": "^1.4.70", "react-native-haptic-feedback": "^2.3.3", @@ -52,6 +53,7 @@ "redux-persist": "^6.0.0", "sockjs-client": "^1.6.1", "text-encoding": "^0.7.0", + "xlsx": "^0.18.5", "yup": "^1.7.1" }, "devDependencies": { diff --git a/src/Screens/Tender/ProceedToBidDetail.tsx b/src/Screens/Tender/ProceedToBidDetail.tsx index f981123..b9ec5ce 100644 --- a/src/Screens/Tender/ProceedToBidDetail.tsx +++ b/src/Screens/Tender/ProceedToBidDetail.tsx @@ -11,6 +11,7 @@ const ProceedToBidDetail = () => { const tenderData = route.params?.tender; const {data, refetch} = useProceedToBid(tenderData?.tenderId); + console.log("proceed to bid tender detail : ", tenderData) const navigation = useNavigation() // Format date from array diff --git a/src/Screens/Tender/component/EditableExcel.tsx b/src/Screens/Tender/component/EditableExcel.tsx new file mode 100644 index 0000000..d5b2a62 --- /dev/null +++ b/src/Screens/Tender/component/EditableExcel.tsx @@ -0,0 +1,242 @@ +import React, { useEffect, useState } from 'react'; +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 { SUBMIT_ENVELOPE_C } from '../../../Services/BBPS/ApiUrls'; + + +// Excel column keys +const QTY_KEY = '__EMPTY_1'; +const RATE_KEY = '__EMPTY_2'; +const VALUE_KEY = '__EMPTY_3'; +const TOTAL_LABEL = 'TOTAL'; + +const EnvelopeCExcelPreview = (tenderId:any) => { + const token = useSelector((state: any) => state.token.token); + const [data, setData] = useState([]); + const [columns, setColumns] = useState([]); + const [loading, setLoading] = useState(false); + + console.log("tender id for excel : ", tenderId) + useEffect(() => { + loadExcelFromApi(); + }, []); + + // 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); + }; + + // 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); + + // LOAD EXCEL + const loadExcelFromApi = async () => { + try { + setLoading(true); + + const response = await axios.get(`${SUBMIT_ENVELOPE_C}${tenderId?.tenderId}`, { + responseType: 'arraybuffer', + headers: { + Authentication: `Bearer ${token.token}`, + }, + }); + + 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: '' }); + + setColumns(Object.keys(rows[0] || {})); + setData(rows); + + await RNFS.writeFile( + `${RNFS.DocumentDirectoryPath}/envelopeC.xlsx`, + base64, + 'base64' + ); + } catch (e) { + Alert.alert('Error', 'Failed to load Excel'); + } finally { + setLoading(false); + } + }; + + // UPDATE CELL (QTY / RATE) + AUTO VALUE + AUTO TOTAL + const updateCell = (rowIndex: number, key: string, value: string) => { + const updated = [...data]; + const row = updated[rowIndex]; + + // TOTAL row locked + if (row['DEMO DEPARTEMENT'] === TOTAL_LABEL) return; + + // Allow only QTY & RATE edit + if (key === QTY_KEY || key === RATE_KEY) { + row[key] = value; + + 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'); + + const base64 = XLSX.write(workbook, { + bookType: 'xlsx', + type: 'base64', + }); + + await RNFS.writeFile( + `${RNFS.DocumentDirectoryPath}/envelopeC_updated.xlsx`, + base64, + 'base64' + ); + + Alert.alert('Success', 'Excel saved with auto-calculation'); + } catch (e) { + Alert.alert('Error', 'Save failed'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + + {/* HEADER */} + + {columns.map(col => ( + + {col} + + ))} + + + {/* DATA */} + + {data.map((row, rowIndex) => ( + + {columns.map(key => { + const isTotal = row['DEMO DEPARTEMENT'] === TOTAL_LABEL; + const isEditable = + !isTotal && (key === QTY_KEY || key === RATE_KEY); + + return ( + + updateCell(rowIndex, key, text) + } + /> + ); + })} + + ))} + + + + + + Save Updated Excel + + + ); +}; + +export default EnvelopeCExcelPreview; + +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: '#2563eb', + padding: 14, + borderRadius: 8, + }, + buttonText: { + color: '#fff', + textAlign: 'center', + fontWeight: '600', + }, +}); diff --git a/src/Screens/Tender/tenderBidding/TenderBidding.tsx b/src/Screens/Tender/tenderBidding/TenderBidding.tsx index efd1b74..044f2ca 100644 --- a/src/Screens/Tender/tenderBidding/TenderBidding.tsx +++ b/src/Screens/Tender/tenderBidding/TenderBidding.tsx @@ -18,6 +18,7 @@ import colors from '../../../Constant/Color'; import { SUBMIT_ENVELOPE_A, SUBMIT_ENVELOPE_B, SUBMIT_ENVELOPE_FILE_A, SUBMIT_ENVELOPE_FILE_B } from '../../../Services/BBPS/ApiUrls'; import { Fontisto } from '@react-native-vector-icons/fontisto'; import { apiCall } from '../../../Axios/Axios'; +import EditableExcel from '../component/EditableExcel'; type EnvelopeType = 'A' | 'B' | 'C'; type EnvelopeFormState = { @@ -130,7 +131,7 @@ const TenderBidding = () => { const labelType = item?.[`labelType${envelopeType}`] || ''; const showInput = - activeEnvelope === envelopeType !== 'C' && + activeEnvelope === envelopeType && !bidData?.[`envelope${envelopeType}SubmittStatus`]; // console.log("tender ana : ", tenderData) @@ -208,17 +209,19 @@ const TenderBidding = () => { keyboardType="numeric" /> )} + this is envelope {activeEnvelope} ) : Envelop {activeEnvelope} unavailable} ); }; - + const hasC = activeEnvelope === 'C' && !tenderData?.envelopeCSubmittStatus; + ; const hasEnvelopes = () => { const hasA = tenderData?.tenderEnvelopeAList?.length > 0; const hasB = tenderData?.tenderEnvelopeBList?.length > 0; - // const hasC = tenderData?.tenderEnvelopeCList?.length > 0; + return hasA || hasB; }; @@ -357,7 +360,7 @@ const TenderBidding = () => { } try { - const res = await apiCall('post',SUBMIT_ENVELOPE_A, payload) + const res = await apiCall('post', SUBMIT_ENVELOPE_A, payload) console.log("res A envelop : ", res) ToastAndroid.show(res.message, ToastAndroid.SHORT) @@ -426,6 +429,9 @@ const TenderBidding = () => { } } + const handleSubmitEnvelopeC = async () => { + Alert.alert('Info', 'Envelope C submission not implemented yet'); + } return ( @@ -482,7 +488,7 @@ const TenderBidding = () => { {/* Envelope A List */} - {tenderData?.[`tenderEnvelope${activeEnvelope}List`]?.length > 0 && activeEnvelope !== 'C' && ( + {tenderData?.[`tenderEnvelope${activeEnvelope}List`]?.length > 0 && ( Envelope {activeEnvelope} Details { renderItem={({ item }) => renderEnvelopeItem({ item, envelopeType: activeEnvelope })} scrollEnabled={false} /> + setCheck(!check)}> {check ? : @@ -501,6 +508,31 @@ const TenderBidding = () => { )} + {activeEnvelope === 'C' && ( + + Envelope C Status + + {tenderData?.envelopeCSubmittStatus ? ( + <> + + Envelope C has been submitted successfully. + + + {tenderData?.envelopeC_Logs && ( + <> + Date: {tenderData.envelopeC_Logs.dateTime} + Message: {tenderData.envelopeC_Logs.logMessage} + + )} + + ) : ( + <> + + + )} + + )} + {hasEnvelopes() ? ( { } if (activeEnvelope === 'A') { handleSubmitEnvelopeA() - } else { + } + else if (activeEnvelope === 'B') { handleSubmitEnvelopeB() } + else { + handleSubmitEnvelopeC() + } }} > {loading ? 'Submitting...' : `Submit Bid (${activeEnvelope})`} diff --git a/src/Services/BBPS/ApiUrls.tsx b/src/Services/BBPS/ApiUrls.tsx index d360396..a8f9c25 100644 --- a/src/Services/BBPS/ApiUrls.tsx +++ b/src/Services/BBPS/ApiUrls.tsx @@ -71,6 +71,7 @@ const SUBMIT_ENVELOPE_A = `${BASE_URL}tenderBidding/getSaveEnvelopeAData` const SUBMIT_ENVELOPE_B = `${BASE_URL}tenderBidding/getSaveEnvelopeBData` const SUBMIT_ENVELOPE_FILE_A = `${BASE_URL}tenderBidding/addDocumentsEnvelopeA?requestDto=` const SUBMIT_ENVELOPE_FILE_B = `${BASE_URL}tenderBidding/addDocumentsEnvelopeB?requestDto=` +const SUBMIT_ENVELOPE_C = `${BASE_URL}eTendering/getEnvelopeCTemplate/tenderId?tenderId=` @@ -137,6 +138,7 @@ export { SUBMIT_ENVELOPE_A, SUBMIT_ENVELOPE_B, SUBMIT_ENVELOPE_FILE_A, - SUBMIT_ENVELOPE_FILE_B + SUBMIT_ENVELOPE_FILE_B, + SUBMIT_ENVELOPE_C }; diff --git a/src/Services/BBPS/Hooks.ts b/src/Services/BBPS/Hooks.ts index 8006ef3..aea8494 100644 --- a/src/Services/BBPS/Hooks.ts +++ b/src/Services/BBPS/Hooks.ts @@ -30,8 +30,6 @@ export const useFetchPurchaseTender = () => { }), initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { - console.log('last page : ', lastPage) - console.log("all page : ", allPages) const { totalCount } = lastPage; const totalPages = Math.ceil(totalCount / 10);