mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-07-01 01:17:04 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user