@@ -15,12 +15,12 @@ app.use(express.json({ limit: '50mb' }))
1515
1616const router = express . Router ( )
1717router . use ( ( req , res , next ) => {
18- const token = req . get ( 'x-auth-token' )
19- if ( ! ! token && token === AUTH_TOKEN ) {
18+ const token = req . get ( 'x-auth-token' )
19+ if ( ! ! token && token === AUTH_TOKEN ) {
2020 next ( )
21- } else {
22- res . status ( 401 ) . json ( { message : 'Invalid auth token' } )
23- }
21+ } else {
22+ res . status ( 401 ) . json ( { message : 'Invalid auth token' } )
23+ }
2424} )
2525
2626router . post ( '/lambda/json-to-excel/from-link' , async ( req , res ) => {
@@ -81,19 +81,19 @@ router.post('/lambda/json-to-excel/common-styled', async (req, res) => {
8181} )
8282
8383router . post ( '/lambda/json-to-excel/client-styled' , async ( req , res ) => {
84- console . log ( 'styled working' )
84+ console . log ( 'client styled working' )
8585 try {
8686 const jsonData = req . body . excel
8787 const excelData = await convertJsonToStyledExcel ( jsonData )
8888 let finalBuffer = excelData
89- if ( jsonData . Countries . data ?. length ) {
90- const sheetName = 'Clients'
89+ if ( jsonData . Lookups . data ?. length ) {
9190 const wb = new ExcelJS . Workbook ( )
9291 await wb . xlsx . load ( excelData )
93- await injectClientTemplateColumnsIntoSheet ( wb , sheetName , jsonData . Countries . data )
92+ const config = req . body . lookupConfig || { }
93+ await injectFormulasIntoSheet ( wb , jsonData . Lookups . data , config )
9494 finalBuffer = await wb . xlsx . writeBuffer ( )
9595 } else {
96- console . log ( '❌ No countries found, skipping injection' )
96+ console . log ( 'No countries found, skipping injection' )
9797 }
9898 const url = await uploadToAWS ( req . body . config , finalBuffer )
9999 return res . json ( { url } )
@@ -136,74 +136,121 @@ const uploadToAWS = async (config, excelData) => {
136136 const response = await s3 . upload ( dataset ) . promise ( )
137137 return response . Location
138138}
139- async function injectClientTemplateColumnsIntoSheet ( workbook , sheetName , data ) {
139+
140+ async function injectFormulasIntoSheet ( workbook , data , config ) {
141+ const sheetName = config . dependentSheet
140142 const sheet = workbook . getWorksheet ( sheetName ) || workbook . worksheets [ 0 ]
141143 if ( ! sheet ) throw new Error ( 'Target sheet not found' )
142- // Create (or reuse) a hidden sheet "Countries"
143- const countrySheet = workbook . getWorksheet ( 'Countries' ) || workbook . addWorksheet ( 'Countries' )
144- countrySheet . state = 'veryHidden'
145- // Headers
146- countrySheet . getCell ( 'A1' ) . value = 'Country'
147- countrySheet . getCell ( 'B1' ) . value = 'Currency'
148- // find max nob length
149- const maxNobs = Math . max ( ...data . map ( c => ( c . nob || [ ] ) . length ) )
150- for ( let j = 0 ; j < maxNobs ; j ++ ) {
151- countrySheet . getCell ( 1 , 3 + j ) . value = `NOB${ j + 1 } `
152- }
153- // Fill rows
154- data . forEach ( ( c , i ) => {
155- const r = i + 2
156- countrySheet . getCell ( r , 1 ) . value = c . country || ''
157- countrySheet . getCell ( r , 2 ) . value = c . currency || ''
158- ; ( c . nob || [ ] ) . forEach ( ( n , j ) => {
159- countrySheet . getCell ( r , 3 + j ) . value = n
144+ const lookupSheet = config . lookupSheet
145+ const hiddenSheet = workbook . getWorksheet ( lookupSheet ) || workbook . addWorksheet ( lookupSheet )
146+ hiddenSheet . state = 'veryHidden'
147+ const { primaryKey, dependentKeys = [ ] , lookupKeys = [ ] } = config
148+ if ( ! primaryKey ) throw new Error ( "Config must specify primaryKey" )
149+ const allGroups = [ primaryKey , ...lookupKeys , ...dependentKeys ] . map ( k => Array . isArray ( k ) ? k : [ k ] )
150+ const canonicalKeys = allGroups . map ( g => g [ 0 ] ) // always first alias = canonical key
151+ const arrayLengths = { }
152+ data . forEach ( ( row ) => {
153+ Object . keys ( row ) . forEach ( ( k ) => {
154+ if ( Array . isArray ( row [ k ] ) ) {
155+ const norm = k . toLowerCase ( )
156+ arrayLengths [ norm ] = Math . max ( arrayLengths [ norm ] || 0 , row [ k ] . length )
157+ }
160158 } )
161- // Named range for each country NOBs
162- const fromCol = 3
163- const toCol = 2 + maxNobs
164- const range = `${ countrySheet . name } !$${ String . fromCharCode ( 65 + fromCol - 1 ) } ${ r } :$${ String . fromCharCode ( 65 + toCol - 1 ) } ${ r } `
165- // countrySheet.workbook.definedNames.addName(c.country.replace(/\s+/g, "_"), range)
159+ } )
160+ // --- Build expanded headers ---
161+ let expandedKeys = [ ]
162+ canonicalKeys . forEach ( ( key ) => {
163+ if ( arrayLengths [ key ] ) {
164+ for ( let i = 0 ; i < arrayLengths [ key ] ; i ++ ) {
165+ expandedKeys . push ( i === 0 ? key : `${ key } _${ i + 1 } ` )
166+ }
167+ } else {
168+ expandedKeys . push ( key )
169+ }
170+ } )
171+ hiddenSheet . getRows ( 1 , hiddenSheet . rowCount ) . forEach ( r => {
172+ r . eachCell ( c => { c . value = null } )
173+ } )
174+ hiddenSheet . getRow ( 1 ) . values = expandedKeys
175+ data . forEach ( ( row , i ) => {
176+ const baseRow = { }
177+ Object . keys ( row ) . forEach ( k => {
178+ baseRow [ k . toLowerCase ( ) ] = row [ k ]
179+ } )
180+ const rowValues = [ ]
181+ canonicalKeys . forEach ( ( key ) => {
182+ const val = baseRow [ key ]
183+ if ( Array . isArray ( val ) ) {
184+ for ( let j = 0 ; j < arrayLengths [ key ] ; j ++ ) {
185+ rowValues . push ( val [ j ] || "" )
186+ }
187+ } else {
188+ rowValues . push ( val || "" )
189+ }
190+ } )
191+ hiddenSheet . getRow ( i + 2 ) . values = rowValues
166192 } )
167193 const lastRow = data . length + 1
168- const countryList = `Countries !$A$2:$A$${ lastRow } `
169- // Find target columns in client sheet
170- const findHeaderCol = ( names ) => {
194+ const primaryRange = `Lookups !$A$2:$A$${ lastRow } `
195+
196+ const findCol = ( aliases ) => {
171197 const headerRow = sheet . getRow ( 1 )
198+ const lookupNames = Array . isArray ( aliases ) ? aliases : [ aliases ]
199+
172200 for ( let col = 1 ; col <= sheet . columnCount ; col ++ ) {
173201 const val = headerRow . getCell ( col ) ?. value
174202 const text = typeof val === 'object'
175203 ? ( val ?. richText ?. map ( rt => rt . text ) . join ( '' ) || val ?. result || '' )
176204 : ( val || '' )
177- if ( names . includes ( String ( text ) . trim ( ) ) ) return col
205+ const normalized = String ( text ) . trim ( ) . toLowerCase ( )
206+
207+ for ( const alias of lookupNames ) {
208+ const normAlias = alias . trim ( ) . toLowerCase ( )
209+ if ( normalized === normAlias ) return col
210+ if ( normalized . replace ( / \s + / g, "_" ) === normAlias ) return col // "Country Code" -> country_code
211+ if ( normalized . replace ( / [ ^ a - z 0 - 9 ] / gi, "" ) === normAlias . replace ( / [ ^ a - z 0 - 9 ] / gi, "" ) ) return col // remove *, etc.
212+ }
178213 }
179214 return null
180215 }
181- let colCountry = findHeaderCol ( [ 'Country*' , 'country' ] )
182- let colNob = findHeaderCol ( [ 'Nature of Business*' , 'category' , 'NoB' ] )
183- let colCurrency = findHeaderCol ( [ 'Currency*' , 'currency' ] )
184- // Apply validations row-wise
216+ // --- Primary key col ---
217+ const colPrimary = findCol ( primaryKey )
218+ if ( ! colPrimary ) throw new Error ( `Primary key column ${ primaryKey } not found in sheet` )
219+
185220 const maxRow = Math . max ( sheet . rowCount , 200 )
186221 for ( let row = 2 ; row <= maxRow ; row ++ ) {
187- const countryCell = sheet . getRow ( row ) . getCell ( colCountry )
188- // Country dropdown
189- countryCell . dataValidation = {
222+ const primaryCell = sheet . getRow ( row ) . getCell ( colPrimary )
223+ //primary dropdown
224+ primaryCell . dataValidation = {
190225 type : 'list' ,
191226 allowBlank : true ,
192- formulae : [ countryList ] ,
227+ formulae : [ primaryRange ] ,
193228 }
194- // Nob dropdown (dependent on country)
195- const nobCell = sheet . getRow ( row ) . getCell ( colNob )
196- const nobFormula = `=OFFSET(Countries!$C$2,MATCH(${ countryCell . address } ,Countries!$A$2:$A$${ lastRow } ,0)-1,0,1,COUNTA(OFFSET(Countries!$C$2,MATCH(${ countryCell . address } ,Countries!$A$2:$A$${ lastRow } ,0)-1,0,1,200)))`
197- nobCell . dataValidation = {
198- type : 'list' ,
199- allowBlank : true ,
200- formulae : [ nobFormula ] ,
229+ //Dependent dropdowns
230+ for ( const depGroup of dependentKeys ) {
231+ const colDep = findCol ( depGroup )
232+ if ( ! colDep ) continue
233+ const depKey = Array . isArray ( depGroup ) ? depGroup [ 0 ] : depGroup
234+ const depCell = sheet . getRow ( row ) . getCell ( colDep )
235+ depCell . dataValidation = {
236+ type : 'list' ,
237+ allowBlank : true ,
238+ formulae : [ `OFFSET(Lookups!$${ String . fromCharCode ( 65 + canonicalKeys . indexOf ( depKey ) ) } $2,MATCH(${ primaryCell . address } ,Lookups!$A$2:$A$${ lastRow } ,0)-1,0,1,
239+ COUNTA(OFFSET(Lookups!$${ String . fromCharCode ( 65 + canonicalKeys . indexOf ( depKey ) ) } $2,MATCH(${ primaryCell . address } ,Lookups!$A$2:$A$${ lastRow } ,0)-1,0,1,50)))`
240+ . replace ( / \s + / g, ' ' ) ] ,
241+ }
201242 }
202- // Currency autofill
203- const currencyCell = sheet . getRow ( row ) . getCell ( colCurrency )
204- currencyCell . value = {
205- formula : `=IF(${ countryCell . address } ="","",VLOOKUP(${ countryCell . address } ,Countries!$A$2:$B$${ lastRow } ,2,FALSE))`
243+ //Lookup autofill
244+ for ( const lookupGroup of lookupKeys ) {
245+ const colLookup = findCol ( lookupGroup )
246+ if ( ! colLookup ) continue
247+ const lookupKey = Array . isArray ( lookupGroup ) ? lookupGroup [ 0 ] : lookupGroup
248+ const lookupCell = sheet . getRow ( row ) . getCell ( colLookup )
249+ lookupCell . value = {
250+ formula : `IF(${ primaryCell . address } ="","",VLOOKUP(${ primaryCell . address } ,Lookups!$A$2:$Z$${ lastRow } ,${ canonicalKeys . indexOf ( lookupKey ) + 1 } ,FALSE))`
251+ }
206252 }
253+
207254 }
208255}
209256
0 commit comments