@@ -267,6 +267,63 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intelligence Dashboard - NEW -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Sleeper Lots -->
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl shadow-md p-6 card-hover">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-purple-900 flex items-center">
|
||||
<i class="fas fa-eye mr-2"></i>Sleeper Lots
|
||||
</h3>
|
||||
<p class="text-xs text-purple-700 mt-1">High interest, low bids</p>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-purple-600" id="sleeper-count">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="showSleeperLots()" class="w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition">
|
||||
<i class="fas fa-search mr-2"></i>View Opportunities
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bargain Lots -->
|
||||
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-xl shadow-md p-6 card-hover">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-green-900 flex items-center">
|
||||
<i class="fas fa-tag mr-2"></i>Bargains
|
||||
</h3>
|
||||
<p class="text-xs text-green-700 mt-1">Below estimate</p>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-green-600" id="bargain-count">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="showBargainLots()" class="w-full bg-green-600 text-white py-2 rounded-lg hover:bg-green-700 transition">
|
||||
<i class="fas fa-search mr-2"></i>Find Deals
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Popular Lots -->
|
||||
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-xl shadow-md p-6 card-hover">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-orange-900 flex items-center">
|
||||
<i class="fas fa-fire mr-2"></i>Hot Lots
|
||||
</h3>
|
||||
<p class="text-xs text-orange-700 mt-1">High competition</p>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-orange-600" id="popular-count">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="showPopularLots()" class="w-full bg-orange-600 text-white py-2 rounded-lg hover:bg-orange-700 transition">
|
||||
<i class="fas fa-search mr-2"></i>View Trending
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics & Performance -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
@@ -548,11 +605,17 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('title')">
|
||||
Title <i class="fas fa-sort ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('followersCount')">
|
||||
Watchers <i class="fas fa-sort ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('currentBid')">
|
||||
Current Bid <i class="fas fa-sort ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Closing Time
|
||||
Est. Range
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Total Cost
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('minutesUntilClose')">
|
||||
Time Left <i class="fas fa-sort ml-1"></i>
|
||||
@@ -641,7 +704,10 @@ let dashboardState = {
|
||||
closingSoon: [],
|
||||
countryDistribution: {},
|
||||
categoryDistribution: {},
|
||||
trendData: {}
|
||||
trendData: {},
|
||||
sleepers: [],
|
||||
bargains: [],
|
||||
popular: []
|
||||
},
|
||||
filters: {
|
||||
auction: '',
|
||||
@@ -748,7 +814,8 @@ async function fetchAllData() {
|
||||
fetchStatistics(),
|
||||
fetchRateLimitStats(),
|
||||
fetchClosingSoon(),
|
||||
fetchChartData()
|
||||
fetchChartData(),
|
||||
fetchIntelligenceData()
|
||||
]);
|
||||
updateLastUpdate();
|
||||
updateDataAge();
|
||||
@@ -766,6 +833,38 @@ async function fetchAllData() {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch intelligence data
|
||||
async function fetchIntelligenceData() {
|
||||
try {
|
||||
// Fetch sleepers
|
||||
const sleepersRes = await fetch('/api/monitor/intelligence/sleepers');
|
||||
if (sleepersRes.ok) {
|
||||
const sleepersData = await sleepersRes.json();
|
||||
document.getElementById('sleeper-count').textContent = sleepersData.count || 0;
|
||||
dashboardState.data.sleepers = sleepersData.lots || [];
|
||||
}
|
||||
|
||||
// Fetch bargains
|
||||
const bargainsRes = await fetch('/api/monitor/intelligence/bargains');
|
||||
if (bargainsRes.ok) {
|
||||
const bargainsData = await bargainsRes.json();
|
||||
document.getElementById('bargain-count').textContent = bargainsData.count || 0;
|
||||
dashboardState.data.bargains = bargainsData.lots || [];
|
||||
}
|
||||
|
||||
// Fetch popular lots
|
||||
const popularRes = await fetch('/api/monitor/intelligence/popular?level=HIGH');
|
||||
if (popularRes.ok) {
|
||||
const popularData = await popularRes.json();
|
||||
document.getElementById('popular-count').textContent = popularData.count || 0;
|
||||
dashboardState.data.popular = popularData.lots || [];
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Intelligence data fetch error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch system status with trends
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
@@ -1179,12 +1278,41 @@ function updateClosingSoonTable(data = null) {
|
||||
const urgencyIcon = minutesLeft < 10 ? 'fa-exclamation-circle' :
|
||||
minutesLeft < 20 ? 'fa-exclamation-triangle' : 'fa-clock';
|
||||
|
||||
// Calculate followers badge
|
||||
const followers = lot.followersCount || 0;
|
||||
const followersBadge = followers > 50 ? 'bg-red-100 text-red-800' :
|
||||
followers > 20 ? 'bg-orange-100 text-orange-800' :
|
||||
followers > 5 ? 'bg-blue-100 text-blue-800' :
|
||||
'bg-gray-100 text-gray-600';
|
||||
|
||||
// Calculate estimate range
|
||||
const estMin = lot.estimatedMin ? (lot.estimatedMin / 100).toFixed(0) : null;
|
||||
const estMax = lot.estimatedMax ? (lot.estimatedMax / 100).toFixed(0) : null;
|
||||
const estimateDisplay = estMin && estMax ? `€${estMin}-${estMax}` : '--';
|
||||
|
||||
// Calculate total cost (including VAT and premium)
|
||||
const currentBid = lot.currentBid || 0;
|
||||
const vat = lot.vat || 0;
|
||||
const premium = lot.buyerPremiumPercentage || 0;
|
||||
const totalCost = currentBid * (1 + (vat/100) + (premium/100));
|
||||
const totalCostDisplay = totalCost > 0 ? `€${totalCost.toFixed(2)}` : '--';
|
||||
|
||||
// Bargain indicator
|
||||
const isBargain = estMin && currentBid < parseFloat(estMin);
|
||||
const bargainBadge = isBargain ? '<span class="ml-1 text-xs bg-green-500 text-white px-1 rounded">DEAL</span>' : '';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${lot.lotId || '--'}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900 max-w-xs truncate" title="${lot.title || 'N/A'}">${lot.title || 'N/A'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-green-600">${lot.currency || 'EUR'} ${lot.currentBid ? parseFloat(lot.currentBid).toFixed(2) : '0.00'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${lot.closingTime ? new Date(lot.closingTime).toLocaleString() : 'N/A'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${followersBadge}">
|
||||
<i class="fas fa-eye mr-1"></i>${followers}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-green-600">${lot.currency || 'EUR'} ${lot.currentBid ? parseFloat(lot.currentBid).toFixed(2) : '0.00'}${bargainBadge}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-xs text-gray-500">${estimateDisplay}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-purple-600" title="Including VAT (${vat}%) + Premium (${premium}%)">${totalCostDisplay}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeColor}">
|
||||
<i class="fas ${urgencyIcon} mr-1"></i>${minutesLeft} min
|
||||
@@ -1394,6 +1522,46 @@ window.addEventListener('offline', () => {
|
||||
showToast('Connection lost - working offline', 'warning');
|
||||
addLog('Network connection lost', 'warning');
|
||||
});
|
||||
|
||||
// Intelligence widget handlers
|
||||
function showSleeperLots() {
|
||||
if (!dashboardState.data.sleepers || dashboardState.data.sleepers.length === 0) {
|
||||
showToast('No sleeper lots found', 'info');
|
||||
return;
|
||||
}
|
||||
dashboardState.data.closingSoon = dashboardState.data.sleepers;
|
||||
applyFilters();
|
||||
showToast(`Showing ${dashboardState.data.sleepers.length} sleeper lots`, 'success');
|
||||
addLog(`Filtered to sleeper lots (high interest, low bids)`);
|
||||
// Scroll to table
|
||||
document.getElementById('closing-soon-table').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function showBargainLots() {
|
||||
if (!dashboardState.data.bargains || dashboardState.data.bargains.length === 0) {
|
||||
showToast('No bargain lots found', 'info');
|
||||
return;
|
||||
}
|
||||
dashboardState.data.closingSoon = dashboardState.data.bargains;
|
||||
applyFilters();
|
||||
showToast(`Showing ${dashboardState.data.bargains.length} bargain lots`, 'success');
|
||||
addLog(`Filtered to bargain lots (below estimate)`);
|
||||
// Scroll to table
|
||||
document.getElementById('closing-soon-table').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function showPopularLots() {
|
||||
if (!dashboardState.data.popular || dashboardState.data.popular.length === 0) {
|
||||
showToast('No popular lots found', 'info');
|
||||
return;
|
||||
}
|
||||
dashboardState.data.closingSoon = dashboardState.data.popular;
|
||||
applyFilters();
|
||||
showToast(`Showing ${dashboardState.data.popular.length} popular lots`, 'success');
|
||||
addLog(`Filtered to popular lots (high followers)`);
|
||||
// Scroll to table
|
||||
document.getElementById('closing-soon-table').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
0
src/main/resources/META-INF/resources/script.js
Normal file
0
src/main/resources/META-INF/resources/script.js
Normal file
0
src/main/resources/META-INF/resources/style.css
Normal file
0
src/main/resources/META-INF/resources/style.css
Normal file
990
src/main/resources/META-INF/resources/valuation-analytics.html
Normal file
990
src/main/resources/META-INF/resources/valuation-analytics.html
Normal file
@@ -0,0 +1,990 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Auctiora - Valuation Analytics</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/3.0.3/plotly.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1e3a8a;
|
||||
--secondary: #3b82f6;
|
||||
--accent: #60a5fa;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%);
|
||||
}
|
||||
|
||||
.formula-card {
|
||||
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-left: 4px solid var(--secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.formula-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.variable-badge {
|
||||
background: linear-gradient(90deg, #dbeafe, #eff6ff);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #1e40af;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
.result-highlight {
|
||||
background: linear-gradient(90deg, #d1fae5, #ecfdf5);
|
||||
border: 2px solid var(--success);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.insight-alert {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
cursor: help;
|
||||
border-bottom: 1px dotted #3b82f6;
|
||||
}
|
||||
|
||||
.tooltip:hover::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #374151;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.math-display {
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
border-left: 4px solid var(--secondary);
|
||||
}
|
||||
|
||||
.sensitivity-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #e5e7eb;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sensitivity-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--secondary);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- Header -->
|
||||
<header class="gradient-bg text-white py-6 shadow-lg">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="/dashboard" class="text-white hover:text-blue-200 transition">
|
||||
<i class="fas fa-arrow-left mr-2"></i>Back to Dashboard
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-1">
|
||||
<i class="fas fa-calculator mr-3"></i>
|
||||
Valuation Analytics Engine
|
||||
</h1>
|
||||
<p class="text-lg opacity-90">Mathematical Framework for Auction Intelligence</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button onclick="exportAnalysis()"
|
||||
class="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg transition">
|
||||
<i class="fas fa-download mr-2"></i>Export Analysis
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
|
||||
<!-- Input Parameters Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
||||
|
||||
<!-- Lot Parameter Inputs -->
|
||||
<div class="lg:col-span-2 bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-edit mr-2 text-blue-600"></i>
|
||||
Input Parameters
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Basic Parameters -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 border-b pb-2">Current State</h3>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">P_current</span>
|
||||
<span class="tooltip" data-tooltip="Current bid in EUR">Current Bid (€):</span>
|
||||
</label>
|
||||
<input type="number" id="p_current" value="7200"
|
||||
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">C_target</span>
|
||||
<span class="tooltip" data-tooltip="Condition score 0-10">Condition Score:</span>
|
||||
</label>
|
||||
<input type="range" id="c_target" min="0" max="10" step="0.1" value="7.5"
|
||||
class="sensitivity-slider"
|
||||
oninput="recalculate()">
|
||||
<span id="c_target_display" class="ml-3 font-mono">7.5</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">T_target</span>
|
||||
<span class="tooltip" data-tooltip="Manufacturing year">Year Made:</span>
|
||||
</label>
|
||||
<input type="number" id="t_target" value="2015"
|
||||
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">W_watch</span>
|
||||
<span class="tooltip" data-tooltip="Number of watchers">Watch Count:</span>
|
||||
</label>
|
||||
<input type="number" id="w_watch" value="87"
|
||||
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Market Context -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 border-b pb-2">Market Context</h3>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">σ_market</span>
|
||||
<span class="tooltip" data-tooltip="Market volatility 0-1">Market Volatility:</span>
|
||||
</label>
|
||||
<input type="range" id="sigma_market" min="0" max="1" step="0.01" value="0.15"
|
||||
class="sensitivity-slider"
|
||||
oninput="recalculate()">
|
||||
<span id="sigma_display" class="ml-3 font-mono">0.15</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">Λ_b</span>
|
||||
<span class="tooltip" data-tooltip="Current bid velocity (bids/min)">Bid Velocity:</span>
|
||||
</label>
|
||||
<input type="number" id="lambda_b" value="2.3" step="0.1"
|
||||
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">t_close</span>
|
||||
<span class="tooltip" data-tooltip="Minutes until lot closes">Time to Close (min):</span>
|
||||
</label>
|
||||
<input type="number" id="t_close" value="45"
|
||||
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<span class="variable-badge mr-2">N_docs</span>
|
||||
<span class="tooltip" data-tooltip="Number of provenance documents">Provenance Docs:</span>
|
||||
</label>
|
||||
<input type="number" id="n_docs" value="2" min="0"
|
||||
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimate Range Inputs -->
|
||||
<div class="mt-6 pt-6 border-t">
|
||||
<h3 class="text-lg font-semibold text-gray-700 mb-3">Auction Estimates</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">Estimated Min (€)</label>
|
||||
<input type="number" id="est_min" value="6000"
|
||||
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">Estimated Max (€)</label>
|
||||
<input type="number" id="est_max" value="9000"
|
||||
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
oninput="recalculate()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Real-time Results -->
|
||||
<div class="space-y-6">
|
||||
<!-- FMV Result -->
|
||||
<div class="result-highlight">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-semibold text-gray-700">Fair Market Value (FMV)</span>
|
||||
<i class="fas fa-chart-line text-green-600"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-green-600" id="fmv_result">€8,500.00</div>
|
||||
<div class="text-xs text-green-700 mt-1" id="fmv_confidence">87% confidence</div>
|
||||
</div>
|
||||
|
||||
<!-- Undervaluation Score -->
|
||||
<div class="result-highlight">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-semibold text-gray-700">Undervaluation Score</span>
|
||||
<i class="fas fa-gem text-purple-600"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold" id="u_score_result">0.153</div>
|
||||
<div class="text-xs mt-1" id="u_interpretation">
|
||||
<span class="text-green-600">15.3% below fair value</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Predicted Final -->
|
||||
<div class="result-highlight">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-semibold text-gray-700">Predicted Final Price</span>
|
||||
<i class="fas fa-crystal-ball text-blue-600"></i>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-blue-600" id="p_final_result">€8,900.00</div>
|
||||
<div class="text-xs text-blue-700 mt-1" id="prediction_range">
|
||||
95% CI: €8,200 - €9,600
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formula Explanations -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
|
||||
<!-- FMV Formula -->
|
||||
<div class="formula-card rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-function mr-2 text-blue-600"></i>
|
||||
1. Fair Market Value (FMV)
|
||||
</h3>
|
||||
<div class="math-display">
|
||||
FMV = <strong>Σ(P<sub>i</sub> · ω<sub>c</sub> · ω<sub>t</sub> · ω<sub>p</sub> · ω<sub>h</sub>) / Σ(ω<sub>c</sub> · ω<sub>t</sub> · ω<sub>p</sub> · ω<sub>h</sub>)</strong>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-2 mt-4">
|
||||
<p><strong>Explanation:</strong> Weighted average of comparable sales where each comparable is weighted by:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li><span class="variable-badge">ω_c</span> Condition similarity (exponential decay)</li>
|
||||
<li><span class="variable-badge">ω_t</span> Age proximity (exponential decay)</li>
|
||||
<li><span class="variable-badge">ω_p</span> Provenance premium (linear boost)</li>
|
||||
<li><span class="variable-badge">ω_h</span> Historical relevance (logistic function)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Undervaluation Score -->
|
||||
<div class="formula-card rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-search-dollar mr-2 text-purple-600"></i>
|
||||
2. Undervaluation Detection
|
||||
</h3>
|
||||
<div class="math-display">
|
||||
U<sub>score</sub> = <strong>(FMV - P<sub>current</sub>)/FMV · σ<sub>market</sub> · (1 + B<sub>velocity</sub>/10) · ln(1 + W<sub>watch</sub>/W<sub>bid</sub>)</strong>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-2 mt-4">
|
||||
<p><strong>Explanation:</strong> Quantifies mispricing opportunity factoring:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Price gap percentage</li>
|
||||
<li>Market volatility multiplier</li>
|
||||
<li>Bid velocity acceleration</li>
|
||||
<li>Watch-to-bid ratio (buyer intent)</li>
|
||||
</ul>
|
||||
<p class="mt-2 p-2 bg-yellow-50 rounded"><strong>Alert threshold:</strong> U<sub>score</sub> > 0.25</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Predicted Final Price -->
|
||||
<div class="formula-card rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-chart-line mr-2 text-green-600"></i>
|
||||
3. Final Price Prediction
|
||||
</h3>
|
||||
<div class="math-display">
|
||||
P̂<sub>final</sub> = <strong>FMV · (1 + ε<sub>bid</sub> + ε<sub>time</sub> + ε<sub>comp</sub>)</strong>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-2 mt-4">
|
||||
<p><strong>Explanation:</strong> Adjusts FMV for auction dynamics:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li><span class="variable-badge">ε_bid</span> Bid momentum (tanh function)</li>
|
||||
<li><span class="variable-badge">ε_time</span> Time pressure (exponential decay)</li>
|
||||
<li><span class="variable-badge">ε_comp</span> Competition level (logarithmic)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition Multiplier -->
|
||||
<div class="formula-card rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-heartbeat mr-2 text-red-600"></i>
|
||||
4. Condition Multiplier
|
||||
</h3>
|
||||
<div class="math-display">
|
||||
M<sub>cond</sub> = <strong>exp(α<sub>c</sub> · √C<sub>target</sub> - β<sub>c</sub>)</strong>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-2 mt-4">
|
||||
<p><strong>Explanation:</strong> Normalizes prices across condition states using square-root scaling:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>C = 10 (mint): <strong>1.48x</strong> premium</li>
|
||||
<li>C = 7.5 (good): <strong>1.12x</strong> premium</li>
|
||||
<li>C = 5 (avg): <strong>0.91x</strong> discount</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy Recommendations -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-800 mb-6 flex items-center">
|
||||
<i class="fas fa-chess mr-2 text-blue-600"></i>
|
||||
Bidding Strategy Recommendations
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="text-center p-6 bg-gradient-to-br from-green-50 to-green-100 rounded-xl">
|
||||
<div class="text-3xl font-bold text-green-600 mb-2" id="strategy_max">€9,200</div>
|
||||
<div class="text-sm text-green-700">Recommended Max Bid</div>
|
||||
<div class="text-xs text-green-600 mt-2" id="max_strategy_type">Standard strategy</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-6 bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl">
|
||||
<div class="text-3xl font-bold text-blue-600 mb-2" id="optimal_timing">10 min</div>
|
||||
<div class="text-sm text-blue-700">Optimal Bid Timing</div>
|
||||
<div class="text-xs text-blue-600 mt-2">Before close</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center p-6 bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl">
|
||||
<div class="text-3xl font-bold text-purple-600 mb-2" id="competition_level">Medium</div>
|
||||
<div class="text-sm text-purple-700">Competition Level</div>
|
||||
<div class="text-xs text-purple-600 mt-2" id="competition_details">2.3 bids/min</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy Details -->
|
||||
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
|
||||
<div id="strategy_details" class="space-y-3">
|
||||
<div class="flex items-start space-x-3">
|
||||
<i class="fas fa-info-circle text-blue-500 mt-1"></i>
|
||||
<div>
|
||||
<strong>Analysis:</strong> <span id="strategy_analysis">Bid velocity is moderate (2.3 bids/min) with high watch count indicating potential sniping.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start space-x-3">
|
||||
<i class="fas fa-lightbulb text-yellow-500 mt-1"></i>
|
||||
<div>
|
||||
<strong>Recommendation:</strong> <span id="strategy_recommendation">Wait until final 10 minutes. Set max bid at €9,200 (7% above FMV) to secure lot while avoiding bidding war.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start space-x-3">
|
||||
<i class="fas fa-exclamation-triangle text-red-500 mt-1"></i>
|
||||
<div>
|
||||
<strong>Risk Factors:</strong> <span id="strategy_risks">High watch-to-bid ratio (87:8) suggests aggressive sniping likely. Reserve may be close to current bid.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sensitivity Analysis -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Condition Sensitivity -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-sliders-h mr-2 text-blue-600"></i>
|
||||
Condition Sensitivity
|
||||
</h3>
|
||||
<div id="condition-chart" style="height: 300px;"></div>
|
||||
<div class="text-sm text-gray-600 mt-3">
|
||||
<p>How FMV changes with condition score. Current: <strong id="current_condition_point">7.5</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Sensitivity -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-clock mr-2 text-green-600"></i>
|
||||
Time Pressure Impact
|
||||
</h3>
|
||||
<div id="time-chart" style="height: 300px;"></div>
|
||||
<div class="text-sm text-gray-600 mt-3">
|
||||
<p>Final price prediction vs. time to close. Current: <strong id="current_time_point">45 min</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Log -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<i class="fas fa-history mr-2 text-purple-600"></i>
|
||||
Calculation Log
|
||||
</h3>
|
||||
<div id="calculation-log" class="space-y-2 max-h-64 overflow-y-auto border rounded-lg p-3 bg-gray-50">
|
||||
<div class="text-sm text-gray-500 flex items-center">
|
||||
<i class="fas fa-info-circle mr-2 text-blue-500"></i>
|
||||
<span>System initialized. Ready for calculations...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-800 text-white py-6 mt-12">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<p class="text-gray-300">
|
||||
<i class="fas fa-calculator mr-2"></i>
|
||||
Auctiora Valuation Engine v2.1 | Mathematical Framework
|
||||
</p>
|
||||
<p class="text-sm text-gray-400 mt-2">
|
||||
Multi-factor weighted valuation with exponential decay models
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Mathematical constants (retained for sensitivity charts and fallback)
|
||||
const CONSTANTS = {
|
||||
lambda_c: 0.693, // Condition decay constant
|
||||
lambda_t: 0.048, // Time decay constant
|
||||
delta_p: 0.15, // Provenance premium coefficient
|
||||
kh: 0.01, // Historical relevance coefficient
|
||||
alpha_c: 0.15, // Condition sensitivity
|
||||
beta_c: 0.40, // Condition baseline offset
|
||||
gamma: 0.25, // Depreciation aggressivity
|
||||
b_threshold: 10, // High velocity threshold
|
||||
phi_1: 0.15, // Bid momentum coefficient 1
|
||||
phi_2: 0.10, // Bid momentum coefficient 2
|
||||
psi: 0.20, // Time pressure coefficient
|
||||
rho: 0.08, // Competition coefficient
|
||||
theta_agg: 0.10, // Aggressive discount target
|
||||
theta_cons: 0.05, // Conservative overbid tolerance
|
||||
delta_margin: 50 // Minimum margin €
|
||||
};
|
||||
|
||||
// Dashboard state
|
||||
let dashboardState = {
|
||||
data: {},
|
||||
filters: {},
|
||||
autoRefresh: true,
|
||||
refreshInterval: 15000,
|
||||
isCalculating: false,
|
||||
apiAvailable: false
|
||||
};
|
||||
|
||||
// Initialize page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
addLog('Initializing valuation engine...');
|
||||
checkAPIHealth();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Check API availability on load
|
||||
async function checkAPIHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/analytics/valuation/health', {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
dashboardState.apiAvailable = true;
|
||||
addLog('API connection established', 'success');
|
||||
showToast('Connected to valuation engine', 'success');
|
||||
} else {
|
||||
dashboardState.apiAvailable = false;
|
||||
addLog('API health check failed - running in offline mode', 'warning');
|
||||
showToast('Running in offline mode', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
dashboardState.apiAvailable = false;
|
||||
addLog('API unavailable - using fallback calculations', 'warning');
|
||||
showToast('Offline mode: using fallback calculations', 'warning');
|
||||
} finally {
|
||||
recalculate();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Input change listeners
|
||||
document.getElementById('c_target').addEventListener('input', function() {
|
||||
document.getElementById('c_target_display').textContent = this.value;
|
||||
recalculate();
|
||||
});
|
||||
|
||||
document.getElementById('sigma_market').addEventListener('input', function() {
|
||||
document.getElementById('sigma_display').textContent = this.value;
|
||||
recalculate();
|
||||
});
|
||||
|
||||
// Add listeners to all input fields
|
||||
const inputs = document.querySelectorAll('input[type="number"]');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('input', debounce(recalculate, 300));
|
||||
});
|
||||
}
|
||||
|
||||
// Debounce function to prevent excessive API calls
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Main recalculation function - calls backend API
|
||||
async function recalculate() {
|
||||
if (dashboardState.isCalculating) return;
|
||||
|
||||
const params = getInputParams();
|
||||
showLoadingState(true);
|
||||
|
||||
if (dashboardState.apiAvailable) {
|
||||
try {
|
||||
const response = await fetch('/api/analytics/valuation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const valuationData = await response.json();
|
||||
dashboardState.data.valuation = valuationData;
|
||||
|
||||
updateAllDisplays(valuationData);
|
||||
addLog(`API calculation completed in ${valuationData.calculationTimeMs}ms`, 'success');
|
||||
showToast('Valuation updated from server', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Valuation API error:', error);
|
||||
addLog(`API error: ${error.message} - falling back to mock calculation`, 'error');
|
||||
showToast(`API failed: ${error.message}`, 'error');
|
||||
await calculateMockFallback(params);
|
||||
}
|
||||
} else {
|
||||
// API not available, use fallback directly
|
||||
await calculateMockFallback(params);
|
||||
}
|
||||
|
||||
showLoadingState(false);
|
||||
}
|
||||
|
||||
// Fallback mock calculation (original logic)
|
||||
async function calculateMockFallback(params) {
|
||||
addLog('Using fallback calculation...', 'info');
|
||||
|
||||
const comparables = [
|
||||
{ price: 8200, condition: 8, year: 2016, provenance: 1, days_ago: 30 },
|
||||
{ price: 7800, condition: 7, year: 2014, provenance: 0, days_ago: 45 },
|
||||
{ price: 8500, condition: 9, year: 2017, provenance: 1, days_ago: 60 },
|
||||
{ price: 7500, condition: 6, year: 2013, provenance: 0, days_ago: 25 }
|
||||
];
|
||||
|
||||
let weightedSum = 0;
|
||||
let weightSum = 0;
|
||||
|
||||
comparables.forEach(comp => {
|
||||
const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition));
|
||||
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year));
|
||||
const wp = 1 + CONSTANTS.delta_p * (params.n_docs > 0 ? 1 : 0 - comp.provenance);
|
||||
const wh = 1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)));
|
||||
|
||||
const weight = wc * wt * wp * wh;
|
||||
weightedSum += comp.price * weight;
|
||||
weightSum += weight;
|
||||
});
|
||||
|
||||
let fmv = weightSum > 0 ? weightedSum / weightSum : (params.est_min + params.est_max) / 2;
|
||||
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.c_target) - CONSTANTS.beta_c);
|
||||
fmv *= mCond;
|
||||
|
||||
if (params.n_docs > 0) {
|
||||
const provPremium = CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs);
|
||||
fmv *= (1 + provPremium);
|
||||
}
|
||||
|
||||
// Create mock response structure matching API format
|
||||
const mockResponse = {
|
||||
fairMarketValue: {
|
||||
value: Math.round(fmv * 100) / 100,
|
||||
conditionMultiplier: Math.round(mCond * 1000) / 1000,
|
||||
provenancePremium: params.n_docs > 0 ? CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs) : 0,
|
||||
comparablesUsed: comparables.length,
|
||||
confidence: 0.85,
|
||||
weightedComparables: comparables.map(comp => ({
|
||||
comparableLotId: `MOCK-${comp.price}`,
|
||||
finalPrice: comp.price,
|
||||
totalWeight: 0.25,
|
||||
components: {
|
||||
conditionWeight: Math.round(Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition)) * 1000) / 1000,
|
||||
timeWeight: Math.round(Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year)) * 1000) / 1000,
|
||||
provenanceWeight: Math.round((1 + CONSTANTS.delta_p * (params.n_docs > 0 ? 1 : 0 - comp.provenance)) * 1000) / 1000,
|
||||
historicalWeight: Math.round((1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)))) * 1000) / 1000
|
||||
}
|
||||
}))
|
||||
},
|
||||
undervaluationScore: calculateUndervaluationScore(params, fmv),
|
||||
pricePrediction: calculateFinalPrice(params, fmv),
|
||||
biddingStrategy: generateBiddingStrategy(params, {value: fmv}, calculateFinalPrice(params, fmv)),
|
||||
calculationTimeMs: 150
|
||||
};
|
||||
|
||||
updateAllDisplays(mockResponse);
|
||||
showToast('Using fallback calculations', 'warning');
|
||||
}
|
||||
|
||||
// Update all displays with unified response format
|
||||
function updateAllDisplays(data) {
|
||||
updateFMVDisplay(data.fairMarketValue);
|
||||
updateUndervaluationDisplay(data.undervaluationScore);
|
||||
updateFinalPriceDisplay(data.pricePrediction);
|
||||
updateStrategyDisplay(data.biddingStrategy);
|
||||
}
|
||||
|
||||
// Show/hide loading state
|
||||
function showLoadingState(isLoading) {
|
||||
dashboardState.isCalculating = isLoading;
|
||||
|
||||
const inputs = document.querySelectorAll('input[type="number"], input[type="range"], select, button:not(#auto-refresh-btn)');
|
||||
inputs.forEach(el => el.disabled = isLoading);
|
||||
|
||||
// Show loading indicators
|
||||
const loadingHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
const elements = ['fmv_result', 'u_score_result', 'p_final_result'];
|
||||
elements.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (isLoading) {
|
||||
el.innerHTML = loadingHTML;
|
||||
el.classList.add('text-blue-600');
|
||||
} else {
|
||||
el.classList.remove('text-blue-600');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get input parameters
|
||||
function getInputParams() {
|
||||
return {
|
||||
lotId: document.getElementById('lot-id')?.value || 'DEMO-LOT-' + Date.now(),
|
||||
currentBid: parseFloat(document.getElementById('p_current').value) || 0,
|
||||
conditionScore: parseFloat(document.getElementById('c_target').value) || 0,
|
||||
manufacturingYear: parseInt(document.getElementById('t_target').value) || 0,
|
||||
watchCount: parseInt(document.getElementById('w_watch').value) || 0,
|
||||
bidCount: parseInt(document.getElementById('w_bid')?.value) || 8,
|
||||
marketVolatility: parseFloat(document.getElementById('sigma_market').value) || 0.15,
|
||||
bidVelocity: parseFloat(document.getElementById('lambda_b').value) || 0,
|
||||
minutesUntilClose: parseInt(document.getElementById('t_close').value) || 0,
|
||||
provenanceDocs: parseInt(document.getElementById('n_docs').value) || 0,
|
||||
estimatedMin: parseFloat(document.getElementById('est_min').value) || 0,
|
||||
estimatedMax: parseFloat(document.getElementById('est_max').value) || 0
|
||||
};
|
||||
}
|
||||
|
||||
// Update displays (adapted for API response format)
|
||||
function updateFMVDisplay(fmvData) {
|
||||
document.getElementById('fmv_result').textContent = `€${fmvData.value.toFixed(2)}`;
|
||||
document.getElementById('fmv_confidence').textContent = `${Math.round(fmvData.confidence * 100)}% confidence`;
|
||||
|
||||
if (fmvData.comparablesUsed) {
|
||||
addLog(`Used ${fmvData.comparablesUsed} comparables for FMV calculation`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUndervaluationDisplay(uScore) {
|
||||
document.getElementById('u_score_result').textContent = uScore.toFixed(3);
|
||||
const interp = uScore > 0.25 ? 'text-green-600' :
|
||||
uScore > 0.15 ? 'text-yellow-600' : 'text-red-600';
|
||||
const text = uScore > 0.25 ? `${(uScore*100).toFixed(1)}% below fair value - STRONG BUY` :
|
||||
uScore > 0.15 ? `${(uScore*100).toFixed(1)}% below fair value - CONSIDER` :
|
||||
'Fairly valued or overpriced';
|
||||
document.getElementById('u_interpretation').innerHTML = `<span class="${interp}">${text}</span>`;
|
||||
}
|
||||
|
||||
function updateFinalPriceDisplay(predictionData) {
|
||||
document.getElementById('p_final_result').textContent = `€${predictionData.predictedPrice.toFixed(2)}`;
|
||||
document.getElementById('prediction_range').textContent =
|
||||
`95% CI: €${predictionData.confidenceIntervalLower.toFixed(0)} - €${predictionData.confidenceIntervalUpper.toFixed(0)}`;
|
||||
|
||||
if (predictionData.components) {
|
||||
const {bidMomentum, timePressure, competition} = predictionData.components;
|
||||
addLog(`Price prediction components: bid=${bidMomentum}, time=${timePressure}, comp=${competition}`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStrategyDisplay(strategyData) {
|
||||
document.getElementById('strategy_max').textContent = `€${strategyData.maxBid.toFixed(0)}`;
|
||||
document.getElementById('optimal_timing').textContent =
|
||||
strategyData.recommendedTimingText || strategyData.recommendedTiming?.replace('_', ' ') || 'FINAL 10 MINUTES';
|
||||
document.getElementById('competition_level').textContent = strategyData.competitionLevel || 'MEDIUM';
|
||||
document.getElementById('competition_details').textContent =
|
||||
`${strategyData.type ? strategyData.type.replace('_', ' ') : 'Standard'} strategy`;
|
||||
|
||||
if (strategyData.analysis) {
|
||||
document.getElementById('strategy_analysis').textContent = strategyData.analysis;
|
||||
}
|
||||
if (strategyData.riskFactors) {
|
||||
document.getElementById('strategy_risks').textContent = strategyData.riskFactors.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate sensitivity charts (using mock calculations)
|
||||
function generateSensitivityCharts() {
|
||||
// Condition sensitivity chart
|
||||
const conditionRange = Array.from({length: 21}, (_, i) => i * 0.5);
|
||||
const conditionValues = conditionRange.map(c => {
|
||||
const params = getInputParams();
|
||||
params.c_target = c;
|
||||
return calculateMockFMV(params); // Use simplified mock for chart
|
||||
});
|
||||
|
||||
Plotly.newPlot('condition-chart', [{
|
||||
x: conditionRange,
|
||||
y: conditionValues,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
line: { color: '#3b82f6', width: 3 },
|
||||
marker: { size: 6 },
|
||||
name: 'FMV vs Condition'
|
||||
}], {
|
||||
xaxis: { title: 'Condition Score (C)' },
|
||||
yaxis: { title: 'FMV (€)' },
|
||||
margin: { t: 20, b: 40, l: 60, r: 20 },
|
||||
font: { size: 12 }
|
||||
}, {responsive: true});
|
||||
|
||||
// Time sensitivity chart
|
||||
const timeRange = Array.from({length: 20}, (_, i) => i * 5);
|
||||
const timeValues = timeRange.map(t => {
|
||||
const params = getInputParams();
|
||||
params.t_close = t;
|
||||
return calculateMockFinalPrice(params, calculateMockFMV(params));
|
||||
});
|
||||
|
||||
Plotly.newPlot('time-chart', [{
|
||||
x: timeRange,
|
||||
y: timeValues,
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
line: { color: '#10b981', width: 3 },
|
||||
marker: { size: 6 },
|
||||
name: 'Predicted Final vs Time'
|
||||
}], {
|
||||
xaxis: { title: 'Time to Close (minutes)' },
|
||||
yaxis: { title: 'Predicted Final Price (€)' },
|
||||
margin: { t: 20, b: 40, l: 60, r: 20 },
|
||||
font: { size: 12 }
|
||||
}, {responsive: true});
|
||||
}
|
||||
|
||||
// Simplified mock FMV for chart generation
|
||||
function calculateMockFMV(params) {
|
||||
const comparables = [
|
||||
{ price: 8200, condition: 8, year: 2016, provenance: 1, days_ago: 30 },
|
||||
{ price: 7800, condition: 7, year: 2014, provenance: 0, days_ago: 45 }
|
||||
];
|
||||
|
||||
let weightedSum = 0;
|
||||
let weightSum = 0;
|
||||
|
||||
comparables.forEach(comp => {
|
||||
const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition));
|
||||
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year));
|
||||
const weight = wc * wt;
|
||||
weightedSum += comp.price * weight;
|
||||
weightSum += weight;
|
||||
});
|
||||
|
||||
let fmv = weightSum > 0 ? weightedSum / weightSum : 8000;
|
||||
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.c_target) - CONSTANTS.beta_c);
|
||||
return fmv * mCond;
|
||||
}
|
||||
|
||||
// Simplified mock final price for chart generation
|
||||
function calculateMockFinalPrice(params, fmv) {
|
||||
const epsilon_bid = Math.tanh(CONSTANTS.phi_1 * params.lambda_b);
|
||||
const epsilon_time = CONSTANTS.psi * Math.exp(-params.t_close / 30);
|
||||
return fmv * (1 + epsilon_bid + epsilon_time);
|
||||
}
|
||||
|
||||
// Activity log
|
||||
function addLog(message, type = 'info') {
|
||||
const log = document.getElementById('calculation-log');
|
||||
const entry = document.createElement('div');
|
||||
|
||||
const iconMap = {
|
||||
success: 'fa-check-circle text-green-600',
|
||||
error: 'fa-exclamation-circle text-red-600',
|
||||
warning: 'fa-exclamation-triangle text-yellow-600',
|
||||
info: 'fa-info-circle text-blue-600'
|
||||
};
|
||||
|
||||
entry.className = 'text-sm text-gray-700 flex items-center space-x-2';
|
||||
entry.innerHTML = `
|
||||
<i class="fas ${iconMap[type]} mt-1 text-xs"></i>
|
||||
<span class="text-gray-400 text-xs">${new Date().toLocaleTimeString()}</span>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
log.insertBefore(entry, log.firstChild);
|
||||
while (log.children.length > 20) log.removeChild(log.lastChild);
|
||||
}
|
||||
|
||||
// Toast notification system
|
||||
function showToast(message, type = 'info', duration = 4000) {
|
||||
const container = document.getElementById('toast-container') || createToastContainer();
|
||||
const toast = document.createElement('div');
|
||||
const iconMap = {
|
||||
success: 'fa-check-circle',
|
||||
error: 'fa-exclamation-circle',
|
||||
warning: 'fa-exclamation-triangle',
|
||||
info: 'fa-info-circle'
|
||||
};
|
||||
|
||||
toast.className = `toast ${type}`;
|
||||
toast.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas ${iconMap[type]} text-lg"></i>
|
||||
<span class="font-medium">${message}</span>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
class="ml-4 text-white/80 hover:text-white">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => toast.classList.add('show'), 100);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
function createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'fixed top-4 right-4 z-50 space-y-2';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
// Export analysis function (uses API data if available)
|
||||
function exportAnalysis() {
|
||||
if (!dashboardState.data.valuation) {
|
||||
showToast('No valuation data to export', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const exportData = {
|
||||
...dashboardState.data.valuation,
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: '2.1',
|
||||
source: dashboardState.apiAvailable ? 'API' : 'FALLBACK'
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `valuation-${exportData.lotId || 'analysis'}-${Date.now()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
addLog('Analysis exported successfully', 'success');
|
||||
showToast('Analysis exported', 'success');
|
||||
}
|
||||
|
||||
// Style for toasts (add to HTML <style> section)
|
||||
const toastStyles = document.createElement('style');
|
||||
toastStyles.textContent = `
|
||||
.toast {
|
||||
transform: translateX(400px);
|
||||
transition: transform 0.3s ease;
|
||||
max-width: 400px;
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
|
||||
color: white;
|
||||
}
|
||||
.toast.show { transform: translateX(0); }
|
||||
.toast.success { background: #10b981; }
|
||||
.toast.error { background: #ef4444; }
|
||||
.toast.warning { background: #f59e0b; }
|
||||
.toast.info { background: #3b82f6; }
|
||||
`;
|
||||
document.head.appendChild(toastStyles);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user