1// ============================================================================ 2// compare.js - Payer comparison tab functions 3// Phase 6D extraction from index.html 4// ============================================================================ 5 6 7 // ============================================================ 8 // PAYER COMPARISON FUNCTIONS 9 // ============================================================ 10 11 let currentComparisonCode = null; 12 let currentComparisonPage = 1; 13 let comparisonTotalPages = 1; 14 let comparisonAutocompleteTimeout = null; 15 16 // Update payer display badges in comparison tab 17 function updateComparisonPayersDisplay() { 18 const container = document.getElementById('comparisonPayersList'); 19 const selectedPayers = window.selectedPayerIds || []; 20 21 if (!container) return; 22 23 if (selectedPayers.length === 0) { 24 container.innerHTML = '<span style="color: #999;">No payers selected</span>'; 25 return; 26 } 27 28 // Use payer names from allDatabases if available 29 const payerNames = selectedPayers.map(id => { 30 if (window.allDatabases) { 31 const db = window.allDatabases.find(d => d.id === id); 32 return db ? db.display_name || db.name : id; 33 } 34 return id; 35 }); 36 37 container.innerHTML = payerNames.map((name, idx) => { 38 const colors = ['#5b6aa0', '#ff6b6b', '#2ecc71', '#f1c40f', '#9b59b6']; 39 const color = colors[idx % colors.length]; 40 return `<span style="background: ${color}; color: white; padding: 6px 12px; border-radius: 20px; font-weight: 500;">${name}</span>`; 41 }).join(''); 42 } 43 44 // Handle autocomplete for comparison search 45 async function handleComparisonAutocomplete(query) { 46 if (comparisonAutocompleteTimeout) { 47 clearTimeout(comparisonAutocompleteTimeout); 48 } 49 50 const container = document.getElementById('comparisonAutocomplete'); 51 if (!container) return; 52 53 if (query.length < 2) { 54 container.style.display = 'none'; 55 return; 56 } 57 58 comparisonAutocompleteTimeout = setTimeout(async () => { 59 try { 60 const selectedPayers = window.selectedPayerIds || []; 61 if (selectedPayers.length < 2) { 62 container.innerHTML = '<div style="padding: 15px; color: #ff6b6b;">Please select at least 2 payers for comparison</div>'; 63 container.style.display = 'block'; 64 return; 65 } 66 67 const dbIds = selectedPayers.join(','); 68 const response = await apiRequest(`/api/compare/search?q=${encodeURIComponent(query)}&db_ids=${dbIds}&limit=10`); 69 70 if (response.results && response.results.length > 0) { 71 container.innerHTML = response.results.map(r => ` 72 <div onclick="selectComparisonSuggestion('${r.billing_code}')" style="padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center;" onmouseover="this.style.background='#f8f9fa'" onmouseout="this.style.background='white'"> 73 <div> 74 <strong style="color: #333;">${r.billing_code}</strong> 75 <span style="color: #666; margin-left: 10px;">${r.description || 'No description'}</span> 76 </div> 77 <span style="background: #e3f2fd; color: #1976d2; padding: 2px 8px; border-radius: 4px; font-size: 0.8em;">${r.payer_count} payer${r.payer_count > 1 ? 's' : ''}</span> 78 </div> 79 `).join(''); 80 container.style.display = 'block'; 81 } else { 82 container.innerHTML = '<div style="padding: 15px; color: #999;">No matching services found</div>'; 83 container.style.display = 'block'; 84 } 85 } catch (err) { 86 console.error('Autocomplete error:', err); 87 container.style.display = 'none'; 88 } 89 }, 300); 90 } 91 92 // Select suggestion from autocomplete 93 function selectComparisonSuggestion(billingCode) { 94 document.getElementById('comparisonSearchInput').value = billingCode; 95 document.getElementById('comparisonAutocomplete').style.display = 'none'; 96 searchForComparison(); 97 } 98 99 // Main comparison search function 100 async function searchForComparison() { 101 const searchInput = document.getElementById('comparisonSearchInput'); 102 const query = searchInput.value.trim(); 103 104 if (!query) { 105 showNotification('Please enter a billing code or service name', 'warning'); 106 return; 107 } 108 109 const selectedPayers = window.selectedPayerIds || []; 110 if (selectedPayers.length < 2) { 111 showNotification('Please select at least 2 payers for comparison', 'warning'); 112 return; 113 } 114 115 // Hide autocomplete 116 document.getElementById('comparisonAutocomplete').style.display = 'none'; 117 118 // Show loading state 119 const statsContainer = document.getElementById('comparisonStatsContainer'); 120 statsContainer.innerHTML = '<div style="text-align: center; padding: 40px;"><div class="loading-spinner"></div><p style="margin-top: 15px; color: #666;">Loading comparison data...</p></div>'; 121 122 document.getElementById('comparisonEmptyState').style.display = 'none'; 123 document.getElementById('comparisonProvidersSection').style.display = 'none'; 124 125 try { 126 const dbIds = selectedPayers.join(','); 127 const response = await apiRequest(`/api/compare/billing-code/${encodeURIComponent(query)}?db_ids=${dbIds}`); 128 129 if (response.billing_code) { 130 currentComparisonCode = response.billing_code; 131 displayComparisonStats(response); 132 133 // Load providers 134 await loadComparisonProviders(response.billing_code, 1); 135 } else { 136 statsContainer.innerHTML = ` 137 <div style="background: white; border-radius: 12px; padding: 40px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> 138 <div style="font-size: 3em; margin-bottom: 15px;"><i data-lucide=search></i></div> 139 <h3 style="color: #333; margin-bottom: 10px;">No Data Found</h3> 140 <p style="color: #666;">No pricing data found for billing code "${query}" across your selected payers.</p> 141 </div> 142 `; 143 } 144 } catch (err) { 145 console.error('Comparison search error:', err); 146 statsContainer.innerHTML = ` 147 <div style="background: #fee; border-radius: 12px; padding: 20px; color: #c00; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> 148 <strong>Error loading comparison data:</strong> ${err.message} 149 </div> 150 `; 151 } 152 } 153 154 // Display comparison stats cards 155 function displayComparisonStats(data) { 156 const container = document.getElementById('comparisonStatsContainer'); 157 158 if (!data.payer_stats || data.payer_stats.length === 0) { 159 container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">No statistics available</div>'; 160 return; 161 } 162 163 // Calculate cross-payer stats 164 const allRates = data.payer_stats.flatMap(p => [p.min_rate, p.max_rate]).filter(r => r !== null && r !== undefined); 165 const overallMin = Math.min(...allRates); 166 const overallMax = Math.max(...allRates); 167 const priceDiff = overallMax - overallMin; 168 const priceDiffPercent = overallMin > 0 ? ((priceDiff / overallMin) * 100).toFixed(1) : 0; 169 170 const colors = ['#5b6aa0', '#ff6b6b', '#2ecc71', '#f1c40f', '#9b59b6']; 171 172 let html = ` 173 <div style="background: white; border-radius: 12px; padding: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 20px;"> 174 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;"> 175 <div> 176 <h3 style="margin: 0 0 5px 0; color: #333;"><i data-lucide=bar-chart-3></i> ${data.billing_code}</h3> 177 <p style="margin: 0; color: #666;">${data.description || 'Healthcare Service'}</p> 178 </div> 179 <div style="text-align: right;"> 180 <div style="font-size: 0.9em; color: #666;">Price Variance</div> 181 <div style="font-size: 1.8em; font-weight: 700; color: ${priceDiffPercent > 50 ? '#ff6b6b' : '#2ecc71'};"> 182 ${priceDiffPercent}% 183 </div> 184 </div> 185 </div> 186 187 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px;"> 188 `; 189
190 data.payer_stats.forEach((payer, idx) => { 191 const color = colors[idx % colors.length]; 192 const isLowest = payer.min_rate === overallMin; 193 const isHighest = payer.max_rate === overallMax; 194 195 html += ` 196 <div style="background: linear-gradient(135deg, ${color}22 0%, ${color}11 100%); border-left: 4px solid ${color}; border-radius: 8px; padding: 20px;"> 197 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> 198 <span style="font-weight: 700; color: #333; font-size: 1.1em;">${payer.payer_name}</span> 199 ${isLowest ? '<span style="background: #2ecc71; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em;">LOWEST</span>' : ''} 200 ${isHighest ? '<span style="background: #ff6b6b; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em;">HIGHEST</span>' : ''} 201 </div> 202 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;"> 203 <div> 204 <div style="font-size: 0.8em; color: #666; margin-bottom: 3px;">Min Rate</div> 205 <div style="font-size: 1.3em; font-weight: 600; color: #2ecc71;">$${payer.min_rate?.toFixed(2) || 'N/A'}</div> 206 </div> 207 <div> 208 <div style="font-size: 0.8em; color: #666; margin-bottom: 3px;">Max Rate</div> 209 <div style="font-size: 1.3em; font-weight: 600; color: #ff6b6b;">$${payer.max_rate?.toFixed(2) || 'N/A'}</div> 210 </div> 211 <div> 212 <div style="font-size: 0.8em; color: #666; margin-bottom: 3px;">Average</div> 213 <div style="font-size: 1.3em; font-weight: 600; color: #333;">$${payer.avg_rate?.toFixed(2) || 'N/A'}</div> 214 </div> 215 <div> 216 <div style="font-size: 0.8em; color: #666; margin-bottom: 3px;">Providers</div> 217 <div style="font-size: 1.3em; font-weight: 600; color: #333;">${payer.provider_count?.toLocaleString() || 0}</div> 218 </div> 219 </div> 220 </div> 221 `; 222 }); 223 224 html += ` 225 </div> 226 227 <!-- Summary Bar --> 228 <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;"> 229 <div style="display: flex; justify-content: space-around; text-align: center; flex-wrap: wrap; gap: 15px;"> 230 <div> 231 <div style="font-size: 0.9em; color: #666;">Lowest Price</div> 232 <div style="font-size: 1.4em; font-weight: 700; color: #2ecc71;">$${overallMin.toFixed(2)}</div> 233 </div> 234 <div> 235 <div style="font-size: 0.9em; color: #666;">Highest Price</div> 236 <div style="font-size: 1.4em; font-weight: 700; color: #ff6b6b;">$${overallMax.toFixed(2)}</div> 237 </div> 238 <div> 239 <div style="font-size: 0.9em; color: #666;">Price Difference</div> 240 <div style="font-size: 1.4em; font-weight: 700; color: #333;">$${priceDiff.toFixed(2)}</div> 241 </div> 242 <div> 243 <div style="font-size: 0.9em; color: #666;">Payers Compared</div> 244 <div style="font-size: 1.4em; font-weight: 700; color: #333;">${data.payer_stats.length}</div> 245 </div> 246 </div> 247 </div> 248 </div> 249 `; 250 251 container.innerHTML = html; 252 } 253 254 // Load comparison providers with pagination 255 async function loadComparisonProviders(billingCode, page = 1) { 256 const section = document.getElementById('comparisonProvidersSection'); 257 const tableContainer = document.getElementById('comparisonProvidersTable'); 258 259 section.style.display = 'block'; 260 tableContainer.innerHTML = '<div style="text-align: center; padding: 40px;"><div class="loading-spinner"></div><p style="margin-top: 15px; color: #666;">Loading provider comparison...</p></div>'; 261 262 const selectedPayers = window.selectedPayerIds || []; 263 const dbIds = selectedPayers.join(','); 264 const sortBy = document.getElementById('comparisonSortBy')?.value || 'n
264ame'; 265 const sortOrder = document.getElementById('comparisonSortOrder')?.value || 'asc'; 266 267 try { 268 const response = await apiRequest(`/api/compare/billing-code/${encodeURIComponent(billingCode)}/providers?db_ids=${dbIds}&page=${page}&per_page=20&sort_by=${sortBy}&sort_order=${sortOrder}`); 269 270 currentComparisonPage = response.page || 1; 271 comparisonTotalPages = response.total_pages || 1; 272 273 displayComparisonProviders(response); 274 renderComparisonPagination(); 275 276 } catch (err) { 277 console.error('Error loading comparison providers:', err); 278 tableContainer.innerHTML = ` 279 <div style="padding: 20px; color: #c00; text-align: center;"> 280 Error loading providers: ${err.message} 281 </div> 282 `; 283 } 284 } 285 286 // Display provider comparison table 287 function displayComparisonProviders(data) { 288 const tableContainer = document.getElementById('comparisonProvidersTable'); 289 const selectedPayers = window.selectedPayerIds || []; 290 291 if (!data.providers || data.providers.length === 0) { 292 tableContainer.innerHTML = '<div style="padding: 40px; text-align: center; color: #999;">No provider data available for comparison</div>'; 293 return; 294 } 295 296 // Get payer names for headers 297 const payerNames = selectedPayers.map(id => { 298 if (window.allDatabases) { 299 const db = window.allDatabases.find(d => d.id === id); 300 return db ? db.display_name || db.name : id; 301 } 302 return id; 303 }); 304 305 const colors = ['#5b6aa0', '#ff6b6b', '#2ecc71', '#f1c40f', '#9b59b6']; 306 307 let html = ` 308 <table style="width: 100%; border-collapse: collapse;"> 309 <thead> 310 <tr style="background: #f8f9fa;"> 311 <th style="padding: 12px; text-align: left; border-bottom: 2px solid #eee; font-weight: 600;">Provider / NPI</th> 312 <th style="padding: 12px; text-align: left; border-bottom: 2px solid #eee; font-weight: 600;">Location</th> 313 ${payerNames.map((name, idx) => ` 314 <th style="padding: 12px; text-align: center; border-bottom: 2px solid #eee; font-weight: 600; background: ${colors[idx % colors.length]}22;">${name}</th> 315 `).join('')} 316 <th style="padding: 12px; text-align: center; border-bottom: 2px solid #eee; font-weight: 600;">Variance</th> 317 </tr> 318 </thead> 319 <tbody> 320 `; 321 322 data.providers.forEach((provider, rowIdx) => { 323 const rates = selectedPayers.map(payerId => provider.rates[payerId]); 324 const validRates = rates.filter(r => r !== null && r !== undefined && r > 0); 325 const minRate = validRates.length > 0 ? Math.min(...validRates) : null; 326 const maxRate = validRates.length > 0 ? Math.max(...validRates) : null; 327 const variance = minRate && maxRate ? ((maxRate - minRate) / minRate * 100).toFixed(1) : null; 328 329 html += ` 330 <tr style="border-bottom: 1px solid #eee;${rowIdx % 2 === 0 ? '' : ' background: #fafafa;'}"> 331 <td style="padding: 12px;"> 332 <div style="font-weight: 600; color: #333;">${provider.provider_name || 'Unknown Provider'}</div> 333 <div style="font-size: 0.85em; color: #666;"> 334 ${provider.npi ? `NPI: ${provider.npi}` : ''} 335 ${provider.tin ? ` | TIN: ${provider.tin}` : ''} 336 </div> 337 </td> 338 <td style="padding: 12px; color: #666; font-size: 0.9em;"> 339 ${provider.city && provider.state ? `${provider.city}, ${provider.state}` : provider.city || provider.state || '-'} 340 </td> 341 ${selectedPayers.map((payerId, idx) => { 342 const rate = provider.rates[payerId]; 343 const isLowest = rate === minRate && validRates.length > 1; 344 const isHighest = rate === maxRate && validRates.length > 1; 345 const color = colors[idx % colors.length]; 346 347 if (rate === null || rate === undefined) { 348 return `<td style="padding: 12px; text-align: center; background: ${color}11; color: #999;">-</td>`;
349 } 350 351 return ` 352 <td style="padding: 12px; text-align: center; background: ${color}11; font-weight: 600; ${isLowest ? 'color: #2ecc71;' : isHighest ? 'color: #ff6b6b;' : 'color: #333;'}"> 353 $${rate.toFixed(2)} 354 ${isLowest ? ' <i data-lucide=check></i>' : ''}${isHighest ? ' <i data-lucide=arrow-up></i>' : ''} 355 </td> 356 `; 357 }).join('')} 358 <td style="padding: 12px; text-align: center;"> 359 ${variance !== null ? ` 360 <span style="font-weight: 600; color: ${parseFloat(variance) > 50 ? '#ff6b6b' : parseFloat(variance) > 20 ? '#f39c12' : '#2ecc71'};"> 361 ${variance}% 362 </span> 363 ` : '-'} 364 </td> 365 </tr> 366 `; 367 }); 368 369 html += ` 370 </tbody> 371 </table> 372 `; 373 374 tableContainer.innerHTML = html; 375 } 376 377 // Render pagination for comparison providers 378 function renderComparisonPagination() { 379 const topPagination = document.getElementById('comparisonPaginationTop'); 380 const bottomPagination = document.getElementById('comparisonPaginationBottom'); 381 382 const paginationHTML = ` 383 <div style="color: #666; font-size: 0.9em;"> 384 Page ${currentComparisonPage} of ${comparisonTotalPages} 385 </div> 386 <div style="display: flex; gap: 10px;"> 387 <button 388 onclick="loadComparisonProviders(currentComparisonCode, 1)" 389 ${currentComparisonPage <= 1 ? 'disabled' : ''} 390 style="padding: 8px 12px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: ${currentComparisonPage <= 1 ? 'not-allowed' : 'pointer'}; opacity: ${currentComparisonPage <= 1 ? '0.5' : '1'};"> 391 <i data-lucide=skip-back></i> First 392 </button> 393 <button 394 onclick="loadComparisonProviders(currentComparisonCode, ${currentComparisonPage - 1})" 395 ${currentComparisonPage <= 1 ? 'disabled' : ''} 396 style="padding: 8px 12px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: ${currentComparisonPage <= 1 ? 'not-allowed' : 'pointer'}; opacity: ${currentComparisonPage <= 1 ? '0.5' : '1'};"> 397 Prev 398 </button> 399 <button 400 onclick="loadComparisonProviders(currentComparisonCode, ${currentComparisonPage + 1})" 401 ${currentComparisonPage >= comparisonTotalPages ? 'disabled' : ''} 402 style="padding: 8px 12px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: ${currentComparisonPage >= comparisonTotalPages ? 'not-allowed' : 'pointer'}; opacity: ${currentComparisonPage >= comparisonTotalPages ? '0.5' : '1'};"> 403 Next 404 </button> 405 <button 406 onclick="loadComparisonProviders(currentComparisonCode, ${comparisonTotalPages})" 407 ${currentComparisonPage >= comparisonTotalPages ? 'disabled' : ''} 408 style="padding: 8px 12px; border: 1px solid #ddd; background: white; border-radius: 6px; cursor: ${currentComparisonPage >= comparisonTotalPages ? 'not-allowed' : 'pointer'}; opacity: ${currentComparisonPage >= comparisonTotalPages ? '0.5' : '1'};"> 409 Last <i data-lucide=skip-forward></i> 410 </button> 411 </div> 412 `; 413 414 if (topPagination) topPagination.innerHTML = paginationHTML; 415 if (bottomPagination) bottomPagination.innerHTML = paginationHTML; 416 } 417 418 // Modal for payer selection (redirect to existing functionality) 419 function openPayerSelectorModal() { 420 // Trigger the existing payer selection modal or dropdown 421 const payerButton = document.querySelector('.payer-selector-btn') || document.querySelector('[oncl
421ick*="togglePayerDropdown"]'); 422 if (payerButton) { 423 payerButton.click(); 424 } else { 425 showNotification('Please use the payer selector in the header to change selections', 'info'); 426 } 427 } 428
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.