fairvalue-api / fairvalue-webapp /src /components /ReportTemplate.tsx
FairValue
feat: upgrade NLP engine with deep search, fallbacks, and neutral signal detection
778ab74
import React, { forwardRef } from 'react'
export interface ReportProps {
form: {
selected_name: string
current_club: string
interested_club: string
contract_years: number
age: number
injuries_24m: number
asking_price: number
market_value_estimation: number
}
result: {
ledger: {
intrinsic_performance_value: number
category: string
depreciation: number
baseline_value: number
external_multiplier: number
hard_cap: number
}
nlp_results?: {
durability: number
recency: number
agent: number
}
nlp_found?: boolean
}
}
export const ReportTemplate = forwardRef<HTMLDivElement, ReportProps>(({ form, result }, ref) => {
const L = result.ledger
const isOverpay = form.asking_price > L.hard_cap
const dateStr = new Date().toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' })
return (
<div
ref={ref}
className="printable-report"
style={{
backgroundColor: '#ffffff',
color: '#0f172a',
fontFamily: "'Inter', sans-serif",
boxSizing: 'border-box',
display: 'none', // Hidden on screen, shown in print via CSS
}}
>
{/* HEADER */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', borderBottom: '2px solid #e2e8f0', paddingBottom: '24px', marginBottom: '32px' }}>
<div>
<h1 style={{ fontSize: '32px', fontWeight: 800, margin: 0, color: '#020617', letterSpacing: '-0.03em' }}>
FairValue Strategic Report
</h1>
<p style={{ fontSize: '14px', color: '#64748b', marginTop: '4px' }}>
AI-Driven Transfer Valuation Intelligence
</p>
</div>
<div style={{ textAlign: 'right', fontSize: '12px', color: '#475569', lineHeight: 1.6 }}>
<div style={{ fontWeight: 600, color: '#0f172a' }}>Lawrence Oladeji</div>
<div>oladeji.lawrence@gmail.com</div>
<div><a href="https://wa.me/2349038819790" style={{ color: '#0f172a', textDecoration: 'none' }}>WhatsApp</a></div>
<div><a href="https://premiership-player-fair-value.vercel.app/" style={{ color: '#2563eb', textDecoration: 'none' }}>Website</a></div>
<div style={{ marginTop: '8px', color: '#94a3b8' }}>Generated: {dateStr}</div>
</div>
</div>
{/* PLAYER PROFILE */}
<div style={{ marginBottom: '32px', pageBreakInside: 'avoid', breakInside: 'avoid' }}>
<h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}>
Player Profile & Parameters
</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0' }}>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Target Player</div>
<div style={{ fontSize: '20px', fontWeight: 700, color: '#0f172a' }}>{form.selected_name || 'N/A'}</div>
</div>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Transfer Direction</div>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#0f172a' }}>
{form.current_club || 'Unknown'} → {form.interested_club || 'Unknown'}
</div>
</div>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Age</div>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#0f172a' }}>{form.age} years</div>
</div>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Contract Remaining</div>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#0f172a' }}>{form.contract_years} years</div>
</div>
</div>
</div>
{/* FINANCIAL VALUATION LEDGER */}
<div style={{ marginBottom: '32px', pageBreakInside: 'avoid', breakInside: 'avoid' }}>
<h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}>
AI Valuation Ledger
</h2>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
<colgroup>
<col style={{ width: '70%' }} />
<col style={{ width: '30%' }} />
</colgroup>
<tbody>
<tr style={{ borderBottom: '1px solid #e2e8f0' }}>
<td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>Intrinsic Performance Value</td>
<td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: '#0f172a' }}>£{L.intrinsic_performance_value.toFixed(1)}m</td>
</tr>
<tr style={{ borderBottom: '1px solid #e2e8f0' }}>
<td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>
Age &amp; Contract Impact (SHAP) — {L.depreciation > 0 ? '📉 Depreciation' : '📈 Appreciation'}
{L.depreciation > 0 && form.age <= 23 && (
<div style={{ fontSize: '11px', color: '#94a3b8', marginTop: '2px' }}>
* Model reflects current output, not future potential
</div>
)}
</td>
<td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: L.depreciation > 0 ? '#ef4444' : '#22c55e' }}>
{L.depreciation > 0 ? '-' : '+'}£{Math.abs(L.depreciation).toFixed(1)}m
</td>
</tr>
<tr style={{ borderBottom: '1px solid #e2e8f0' }}>
<td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>ML Baseline Value</td>
<td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: '#0f172a' }}>£{L.baseline_value.toFixed(1)}m</td>
</tr>
<tr style={{ borderBottom: '1px solid #e2e8f0' }}>
<td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>Live NLP Market Multiplier</td>
<td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: L.external_multiplier > 1 ? '#22c55e' : '#ef4444' }}>
×{L.external_multiplier.toFixed(3)}
</td>
</tr>
<tr style={{ backgroundColor: '#f0fdf4', border: '1px solid #bbf7d0' }}>
<td style={{ padding: '16px 12px', color: '#166534', fontWeight: 700, fontSize: '16px' }}>Calculated Fair Value (Ceiling)</td>
<td style={{ padding: '16px 12px', textAlign: 'right', fontWeight: 800, fontSize: '20px', color: '#16a34a' }}>
£{L.hard_cap.toFixed(1)}m
</td>
</tr>
<tr style={{ backgroundColor: '#fefce8', border: '1px solid #fde68a' }}>
<td style={{ padding: '12px 12px', color: '#92400e', fontWeight: 600, fontSize: '13px' }}>
🎯 Recommended Opening Bid
</td>
<td style={{ padding: '12px 12px', textAlign: 'right', fontWeight: 700, fontSize: '15px', color: '#b45309' }}>
£{(L.hard_cap * 0.85).toFixed(1)}m
</td>
</tr>
<tr style={{ backgroundColor: '#fff7ed', border: '1px solid #fed7aa' }}>
<td style={{ padding: '12px 12px', color: '#7c2d12', fontWeight: 600, fontSize: '13px' }}>
⚠️ Walk-Away Ceiling
</td>
<td style={{ padding: '12px 12px', textAlign: 'right', fontWeight: 700, fontSize: '15px', color: '#dc2626' }}>
£{L.hard_cap.toFixed(1)}m
</td>
</tr>
</tbody>
</table>
</div>
{/* NLP INTELLIGENCE BREAKDOWN */}
{result.nlp_results && (
<div style={{ marginBottom: '32px', pageBreakInside: 'avoid', breakInside: 'avoid' }}>
<h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}>
Live Market Intelligence (NLP Breakdown)
</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px', background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0' }}>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Recent Form &amp; Impact</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.recency ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.recency ?? 0) < 0 ? '#ef4444' : '#22c55e' }}>
{(result.nlp_results?.recency ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.recency ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.recency ?? 0).toFixed(2)}`}
</div>
</div>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Injury / Availability</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.durability ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.durability ?? 0) < 0 ? '#ef4444' : '#0f172a' }}>
{(result.nlp_results?.durability ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.durability ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.durability ?? 0).toFixed(2)}`}
</div>
</div>
<div>
<div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Transfer Speculation</div>
<div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.agent ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.agent ?? 0) < 0 ? '#ef4444' : '#22c55e' }}>
{(result.nlp_results?.agent ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.agent ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.agent ?? 0).toFixed(2)}`}
</div>
</div>
</div>
<div style={{ fontSize: '13px', color: '#64748b', marginTop: '12px', lineHeight: 1.5 }}>
<strong>How this works:</strong> The Live NLP Market Multiplier (currently <strong>×{L.external_multiplier.toFixed(3)}</strong>) is calculated from the sentiment scores above. A multiplier of exactly 1.000 means perfectly neutral market hype. A score above 1.0 indicates high demand and positive news, allowing the selling club to charge a premium. A score below 1.0 indicates negative press (e.g. poor form or injury history), creating leverage to negotiate a discount.
</div>
</div>
)}
{/* VERDICT & SUMMARY */}
<div style={{ marginBottom: '40px', pageBreakBefore: 'always', breakBefore: 'page' }}>
<h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}>
Executive Summary & Verdict
</h2>
<div style={{ padding: '24px', background: isOverpay ? '#fef2f2' : '#f0fdf4', border: `1px solid ${isOverpay ? '#fecaca' : '#bbf7d0'}`, borderRadius: '12px' }}>
<p style={{ fontSize: '15px', color: isOverpay ? '#991b1b' : '#166534', lineHeight: 1.6, marginBottom: '16px' }}>
<strong>Negotiation Intel:</strong> Based on the real-time evaluation engine, {form.selected_name}'s intrinsic value sits at £{L.intrinsic_performance_value.toFixed(1)}m.
Age &amp; contract dynamics apply a {L.depreciation > 0 ? `depreciation of -£${Math.abs(L.depreciation).toFixed(1)}m` : `appreciation of +£${Math.abs(L.depreciation).toFixed(1)}m`},
with live NLP market sentiment at ×{L.external_multiplier.toFixed(3)}. The absolute financial ceiling (Fair Value) is <strong>£{L.hard_cap.toFixed(1)}m</strong>.
<br/><br/>
<strong>Validation:</strong> {(result.nlp_results?.recency ?? 0) > 0 ? "Player's recent form commands a market premium." : "Recent form is suboptimal — leverage this to negotiate a lower fee."} {L.depreciation > 0 ? `Age & contract profile adds £${Math.abs(L.depreciation).toFixed(1)}m in depreciation risk — use this as a negotiation lever.` : `Young age and contract security add £${Math.abs(L.depreciation).toFixed(1)}m in appreciation value, justifying a strong offer.`}
{L.depreciation > 0 && form.age <= 23 && (
<span style={{ display: 'block', marginTop: '8px', fontSize: '12px', color: '#64748b' }}>
ℹ️ Note: Depreciation reflects the player's current statistical output stage vs. their peak. Young players may command a future-value premium not captured in this model.
</span>
)}
</p>
<div style={{ fontSize: '18px', fontWeight: 800, color: isOverpay ? '#dc2626' : '#16a34a', borderTop: `1px solid ${isOverpay ? '#fca5a5' : '#86efac'}`, paddingTop: '16px' }}>
VERDICT: {isOverpay ? 'OVERPAY RISK' : 'FAIR DEAL - PROCEED WITH CONFIDENCE'}
</div>
<div style={{ fontSize: '14px', color: isOverpay ? '#b91c1c' : '#15803d', marginTop: '6px' }}>
The selling club's asking price of £{form.asking_price}m {isOverpay ? 'exceeds' : 'is safely within'} our calculated Fair Value.
</div>
</div>
</div>
{/* DEFINITION OF TERMS */}
<div style={{ borderTop: '2px solid #e2e8f0', paddingTop: '24px', pageBreakInside: 'avoid', breakInside: 'avoid' }}>
<h2 style={{ fontSize: '14px', fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '12px' }}>
Definition of Terms
</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', fontSize: '12px', color: '#475569', lineHeight: 1.5 }}>
<div>
<strong style={{ color: '#0f172a' }}>Intrinsic Performance Value:</strong> Player's raw talent value derived purely from on-pitch statistics.
</div>
<div>
<strong style={{ color: '#0f172a' }}>Age & Contract Impact:</strong> Financial adjustment based on age and remaining contract years.
</div>
<div>
<strong style={{ color: '#0f172a' }}>NLP Multiplier:</strong> Live web-scraping adjustment factor. Values {'>'} 1.0 indicate positive hype (premium), while values {'<'} 1.0 indicate negative press (discount).
</div>
<div>
<strong style={{ color: '#0f172a' }}>Fair Value:</strong> The absolute maximum recommended price (financial ceiling).
</div>
</div>
</div>
</div>
)
})