feat: tek birleştirilmiş JSON yapısına geçiş + sosyal kanıt fallback

Ne yaptık:
- data_consolidator.py: Tüm normalizasyon ve hesaplama mantığını main.py'den çıkardık
- Dashboard endpoint 1150 satırdan 25 satıra düştü (main.py -1730/+1880 net)
- Enrichment bitince otomatik konsolide dosya oluşturuluyor (report_{id}_data.json)
- Eski raporlar ilk dashboard isteğinde lazy migration ile konsolide ediliyor
- Trendyol API artık order-count döndürmediği için baskets fallback eklendi
- Inline socialProofs (scrape) > enrichment API öncelik sırası uygulandı
- Frontend KPI başlıkları orders/baskets durumuna göre dinamik değişiyor
- logging_config.py, category_seeder.py, alembic migration eklendi
- Playwright ile 9 tab test edildi, tüm veriler doğru

Neden yaptık:
- 3 farklı kaynaktan her istekte birleştirme yapılması veri tutarsızlığına ve yavaşlığa yol açıyordu
- Tek konsolide JSON dosyası ile dashboard anında yükleniyor
- Trendyol API değişikliği nedeniyle sipariş verisi kayboluyordu, baskets fallback ile çözüldü

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
furkanyigit34
2026-03-28 22:25:25 +03:00
parent 187c59ec9b
commit ce1dc1e25f
15 changed files with 1878 additions and 1459 deletions

View File

@@ -99,17 +99,27 @@ function ReportDashboard() {
const products = dashboardData.all_products
const totalProducts = products.length
const totalOrders = products.reduce((sum, p) => sum + (p.orders || 0), 0)
const rawOrders = products.reduce((sum, p) => sum + (p.orders || 0), 0)
const totalBaskets = products.reduce((sum, p) => sum + (p.baskets || 0), 0)
// Trendyol API artık order-count döndürmüyor — orders > 0 ise onu, yoksa baskets'ı kullan
const totalOrders = rawOrders > 0 ? rawOrders : totalBaskets
const ordersLabel = rawOrders > 0 ? 'orders' : 'baskets'
const totalViews = products.reduce((sum, p) => sum + (p.page_views || 0), 0)
const totalFavorites = products.reduce((sum, p) => sum + (p.favorites || 0), 0)
const avgPrice = products.reduce((sum, p) => sum + (p.price || 0), 0) / totalProducts
const totalRevenue = products.reduce((sum, p) => sum + ((p.price || 0) * (p.orders || 0)), 0)
const totalRevenue = rawOrders > 0
? products.reduce((sum, p) => sum + ((p.price || 0) * (p.orders || 0)), 0)
: products.reduce((sum, p) => sum + ((p.price || 0) * (p.baskets || 0)), 0)
const kpis = {
totalProducts,
totalOrders,
totalBaskets,
totalViews,
totalFavorites,
avgPrice: Math.round(avgPrice),
totalRevenue: Math.round(totalRevenue)
totalRevenue: Math.round(totalRevenue),
ordersLabel
}
console.log('✅ [KPI] Calculated KPIs:', kpis)

View File

@@ -12,8 +12,8 @@ export default function HiddenChampionsTab({ reportId }) {
// Filters
const [minRating, setMinRating] = useState(4.0)
const [maxReview, setMaxReview] = useState(100)
const [minOrders, setMinOrders] = useState(5)
const [sortKey, setSortKey] = useState('performance_score')
const [minOrders, setMinOrders] = useState(0)
const [sortKey, setSortKey] = useState('hidden_champion_score')
const [sortDir, setSortDir] = useState('desc')
const [showFilters, setShowFilters] = useState(false)
@@ -41,9 +41,9 @@ export default function HiddenChampionsTab({ reportId }) {
// Filtered & sorted products
const filteredProducts = useMemo(() => {
if (!data?.products) return []
if (!data?.hidden_champions) return []
return data.products
return data.hidden_champions
.filter(p => {
const rating = p.rating || 0
const reviewCount = p.review_count || p.reviewCount || 0
@@ -230,10 +230,10 @@ export default function HiddenChampionsTab({ reportId }) {
</th>
<th
className="text-right px-4 py-3 font-medium text-slate-500 cursor-pointer hover:text-slate-700"
onClick={() => handleSort('performance_score')}
onClick={() => handleSort('hidden_champion_score')}
>
<div className="flex items-center justify-end gap-1">
Skor <SortIcon column="performance_score" />
Skor <SortIcon column="hidden_champion_score" />
</div>
</th>
</tr>
@@ -287,13 +287,13 @@ export default function HiddenChampionsTab({ reportId }) {
</td>
<td className="px-4 py-3 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold ${
(product.performance_score || 0) >= 70
(product.hidden_champion_score || 0) >= 70
? 'bg-emerald-100 text-emerald-700'
: (product.performance_score || 0) >= 40
: (product.hidden_champion_score || 0) >= 40
? 'bg-amber-100 text-amber-700'
: 'bg-slate-100 text-slate-600'
}`}>
{(product.performance_score || 0).toFixed(0)}
{(product.hidden_champion_score || 0).toFixed(0)}
</span>
</td>
</tr>

View File

@@ -90,21 +90,21 @@ export default function OverviewTab({
? (sortedPrices[sortedPrices.length / 2 - 1] + sortedPrices[sortedPrices.length / 2]) / 2
: sortedPrices[Math.floor(sortedPrices.length / 2)]
const bucketCount = 10
const range = max - min || 1
const bucketSize = range / bucketCount
// Use predefined price ranges for meaningful distribution
const ranges = [
[0, 50], [50, 100], [100, 200], [200, 500],
[500, 1000], [1000, 2000], [2000, 5000], [5000, 10000], [10000, Infinity]
]
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
range: `${Math.round(min + i * bucketSize)}-${Math.round(min + (i + 1) * bucketSize)}`,
min: min + i * bucketSize,
max: min + (i + 1) * bucketSize,
count: 0
}))
prices.forEach(price => {
const idx = Math.min(Math.floor((price - min) / bucketSize), bucketCount - 1)
buckets[idx].count++
})
// Filter out empty ranges and build buckets
const buckets = ranges
.map(([lo, hi]) => ({
range: hi === Infinity ? `${lo.toLocaleString('tr-TR')}+` : `${lo.toLocaleString('tr-TR')}-${hi.toLocaleString('tr-TR')}`,
min: lo,
max: hi,
count: prices.filter(p => p >= lo && (hi === Infinity ? true : p < hi)).length
}))
.filter(b => b.count > 0)
return { buckets, mean: Math.round(mean), median: Math.round(median) }
}, [allProducts])
@@ -186,7 +186,7 @@ export default function OverviewTab({
color="blue"
/>
<KpiCard
title="Toplam Satın Alma"
title={overviewKPIs.ordersLabel === 'baskets' ? 'Toplam Sepete Ekleme' : 'Toplam Satın Alma'}
value={overviewKPIs.totalOrders.toLocaleString('tr-TR')}
icon={ShoppingCart}
color="emerald"
@@ -198,7 +198,7 @@ export default function OverviewTab({
color="violet"
/>
<KpiCard
title="Toplam Ciro"
title={overviewKPIs.ordersLabel === 'baskets' ? 'Tahmini Ciro (Sepet)' : 'Toplam Ciro'}
value={`${(overviewKPIs.totalRevenue || 0).toLocaleString('tr-TR')}`}
icon={DollarSign}
color="orange"
@@ -359,10 +359,10 @@ export default function OverviewTab({
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0' }}
/>
<ReferenceLine
x={priceDistribution.buckets.findIndex(b => b.min <= priceDistribution.mean && b.max > priceDistribution.mean)}
x={(priceDistribution.buckets.find(b => b.min <= priceDistribution.mean && (b.max === Infinity || b.max > priceDistribution.mean)) || {}).range}
stroke="#f97316"
strokeDasharray="5 5"
label={{ value: `Ort: ₺${priceDistribution.mean}`, fill: '#f97316', fontSize: 11, position: 'top' }}
label={{ value: `Ort: ₺${priceDistribution.mean.toLocaleString('tr-TR')}`, fill: '#f97316', fontSize: 11, position: 'top' }}
/>
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} label={{ position: 'top', fill: '#64748b', fontSize: 11 }} />
</BarChart>