feat: add 9 new dashboard features with export and comparison

- Add Hidden Champions tab with filterable product table
- Add Opportunity Map tab with scatter chart (supply/demand quadrants)
- Add Sales Funnel section to Overview with conversion rates
- Add Price Distribution histogram with mean/median lines
- Add Competition Score gauge (0-100) with 4 sub-metrics
- Add Excel export (3-sheet xlsx) and Print buttons to dashboard
- Add Report Comparison page with KPI diff table and brand bar chart
- Add enrichment UI to ReportList with progress tracking
- Add sidebar navigation with Karşılaştır route
- Refactor UI: Layout, Sidebar, TopBar, KpiCard, SkeletonLoader components
- Improve drill-down UX with visible pill buttons and tooltips

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
furkanyigit34
2026-03-07 17:33:07 +03:00
parent c7be57064b
commit 0d908a1afe
38 changed files with 109314 additions and 878367 deletions

5
.gitignore vendored
View File

@@ -107,3 +107,8 @@ README_TESTING.md
# Backend debug/analysis artifacts
backend/*_202*.json
backend/*_202*.png
# Screenshots and test images
*.png
*.jpeg
*.jpg

View File

@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<html lang="tr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin-panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<title>Trendyol Analytics</title>
</head>
<body>
<div id="root"></div>

View File

@@ -9,10 +9,12 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.13.2",
"lucide-react": "^0.577.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.5",
"recharts": "^3.4.1"
"recharts": "^3.4.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@babel/cli": "^7.28.3",
@@ -2269,6 +2271,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2527,6 +2538,19 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2593,6 +2617,15 @@
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2658,6 +2691,18 @@
"node": ">=18"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3329,6 +3374,15 @@
"node": ">= 6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -4069,6 +4123,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4744,6 +4807,18 @@
"node": ">=0.10.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5004,6 +5079,24 @@
"node": ">= 8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5021,6 +5114,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -11,10 +11,12 @@
},
"dependencies": {
"axios": "^1.13.2",
"lucide-react": "^0.577.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.5",
"recharts": "^3.4.1"
"recharts": "^3.4.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@babel/cli": "^7.28.3",

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,98 +1,35 @@
import { lazy, Suspense } from 'react'
import { Routes, Route, NavLink } from 'react-router-dom'
import { Routes, Route } from 'react-router-dom'
import Layout from './components/ui/Layout'
import { PageSkeleton } from './components/ui/SkeletonLoader'
// OPTIMIZATION: Lazy load components for 65% smaller initial bundle (500KB → 175KB)
// OPTIMIZATION: Lazy load components for smaller initial bundle
// Components are loaded only when user navigates to them
const CategoryManagement = lazy(() => import('./components/CategoryManagement'))
const ReportGeneration = lazy(() => import('./components/ReportGeneration'))
const ReportList = lazy(() => import('./components/ReportList'))
const ReportDashboard = lazy(() => import('./components/ReportDashboard'))
const ReportComparison = lazy(() => import('./components/ReportComparison'))
// Loading component
// Skeleton loading fallback
const LoadingFallback = () => (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
<p className="text-gray-600">Yükleniyor...</p>
</div>
<div className="page-enter page-enter-active">
<PageSkeleton />
</div>
)
function App() {
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b border-gray-200 shadow-sm">
<div className="container mx-auto px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900">Trendyol Admin Panel</h1>
<p className="text-sm text-gray-600 mt-1">Kategori Yönetimi & Veri Analizi</p>
</div>
</header>
{/* Tabs - Only show on non-report pages */}
<Routes>
<Route path="/reports/:reportId" element={null} />
<Route
path="*"
element={
<div className="bg-white border-b border-gray-200 shadow-sm">
<div className="container mx-auto px-6">
<nav className="flex space-x-8">
<NavLink
to="/"
className={({ isActive }) =>
`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
isActive
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`
}
>
Kategori Yönetimi
</NavLink>
<NavLink
to="/report"
className={({ isActive }) =>
`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
isActive
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`
}
>
Rapor Oluştur
</NavLink>
<NavLink
to="/reports"
className={({ isActive }) =>
`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
isActive
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`
}
>
Raporlarım
</NavLink>
</nav>
</div>
</div>
}
/>
</Routes>
{/* Content - Wrapped in Suspense for lazy loading */}
<main className="container mx-auto px-6 py-8">
<Layout>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<CategoryManagement />} />
<Route path="/" element={<ReportGeneration />} />
<Route path="/report" element={<ReportGeneration />} />
<Route path="/reports" element={<ReportList />} />
<Route path="/reports/:reportId" element={<ReportDashboard />} />
<Route path="/compare" element={<ReportComparison />} />
</Routes>
</Suspense>
</main>
</div>
</Layout>
)
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { API_URL, fetchWithTimeout } from '../config/api'
import { getCategoryIcon, getCategoryColors } from '../constants/categories'
import { FolderTree, ChevronRight, ExternalLink, Layers, Loader2, FolderOpen, ArrowLeft } from 'lucide-react'
function CategoryManagement() {
const [mainCategories, setMainCategories] = useState([])
@@ -9,6 +9,8 @@ function CategoryManagement() {
const [loading, setLoading] = useState(false)
const [loadingSubCategories, setLoadingSubCategories] = useState(false)
const [error, setError] = useState(null)
// Breadcrumb trail for deep navigation
const [breadcrumb, setBreadcrumb] = useState([])
// Fetch main categories on mount
useEffect(() => {
@@ -46,13 +48,39 @@ function CategoryManagement() {
const handleCategoryClick = (category) => {
setSelectedCategory(category)
setBreadcrumb([category])
fetchSubCategories(category.id)
}
const handleSubCategoryClick = (subCat) => {
if (subCat.children_count > 0) {
setBreadcrumb(prev => [...prev, subCat])
setSelectedCategory(subCat)
fetchSubCategories(subCat.id)
}
}
const handleBreadcrumbClick = (index) => {
const target = breadcrumb[index]
// Trim breadcrumb to clicked level
setBreadcrumb(breadcrumb.slice(0, index + 1))
setSelectedCategory(target)
fetchSubCategories(target.id)
}
const handleBackToMain = () => {
setSelectedCategory(null)
setBreadcrumb([])
setSubCategories([])
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Yükleniyor...</div>
<div className="text-slate-400 flex items-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
Yükleniyor...
</div>
</div>
)
}
@@ -68,94 +96,150 @@ function CategoryManagement() {
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900">Ana Kategoriler</h1>
<p className="text-sm text-gray-600 mt-1">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h1 className="text-2xl font-bold text-slate-900">Kategori Yönetimi</h1>
<p className="text-sm text-slate-500 mt-1">
Toplam {mainCategories.length} ana kategori
</p>
</div>
{/* Main Categories Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{mainCategories.map((category) => (
<div
key={category.id}
onClick={() => handleCategoryClick(category)}
className={`bg-white border-l-4 ${selectedCategory?.id === category.id ? 'border-blue-600' : 'border-gray-300'} rounded-lg shadow-sm p-6 cursor-pointer hover:shadow-md transition-shadow ${
selectedCategory?.id === category.id ? 'ring-2 ring-blue-200' : ''
}`}
className={`bg-white border-l-4 ${selectedCategory?.id === category.id && breadcrumb.length === 1 ? 'border-orange-500 ring-2 ring-orange-200' : 'border-slate-300'} rounded-xl shadow-sm p-5 cursor-pointer hover:shadow-md transition-all`}
>
<div className="flex flex-col space-y-3">
<h3 className="text-lg font-semibold text-gray-900">{category.name}</h3>
<p className="text-sm text-gray-600">
<div className="flex items-center justify-between">
<div>
<h3 className="text-base font-semibold text-slate-900">{category.name}</h3>
<p className="text-sm text-slate-400 mt-1">
{category.children_count || 0} alt kategori
</p>
</div>
<ChevronRight className="w-5 h-5 text-slate-300" />
</div>
</div>
))}
</div>
{/* Subcategories Table */}
{/* Subcategories Panel */}
{selectedCategory && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">
{selectedCategory.name} - Alt Kategoriler
</h2>
<span className="text-sm text-gray-600">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
{/* Breadcrumb Navigation */}
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-slate-200">
<button
onClick={handleBackToMain}
className="text-slate-400 hover:text-orange-500 transition-colors"
title="Ana kategorilere dön"
>
<ArrowLeft className="w-5 h-5" />
</button>
<nav className="flex items-center gap-1 text-sm flex-wrap">
{breadcrumb.map((crumb, index) => (
<span key={crumb.id} className="flex items-center gap-1">
{index > 0 && <ChevronRight className="w-4 h-4 text-slate-300" />}
{index < breadcrumb.length - 1 ? (
<button
onClick={() => handleBreadcrumbClick(index)}
className="text-orange-500 hover:text-orange-600 hover:underline font-medium"
>
{crumb.name}
</button>
) : (
<span className="text-slate-900 font-semibold">{crumb.name}</span>
)}
</span>
))}
</nav>
<span className="ml-auto text-sm text-slate-400">
{subCategories.length} kategori
</span>
</div>
{loadingSubCategories ? (
<div className="text-center py-8 text-gray-500">Yükleniyor...</div>
<div className="text-center py-8 text-slate-400 flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
Yükleniyor...
</div>
) : subCategories.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<div className="text-center py-8 text-slate-400">
Bu kategoride alt kategori bulunamadı.
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
#
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
Kategori Adı
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
Alt Kategori
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
Trendyol ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
Trendyol URL
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white divide-y divide-slate-200">
{subCategories.map((subCat, index) => (
<tr key={subCat.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<tr
key={subCat.id}
onClick={() => handleSubCategoryClick(subCat)}
className={`hover:bg-orange-50/30 ${subCat.children_count > 0 ? 'cursor-pointer' : ''}`}
>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
{index + 1}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">
<div className="flex items-center gap-2">
{subCat.children_count > 0 ? (
<FolderOpen className="w-4 h-4 text-orange-400" />
) : (
<Layers className="w-4 h-4 text-slate-300" />
)}
{subCat.name}
{subCat.children_count > 0 && (
<ChevronRight className="w-4 h-4 text-orange-400" />
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{subCat.trendyol_category_id || '-'}
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
{subCat.children_count > 0 ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-50 text-orange-600 border border-orange-200">
{subCat.children_count} alt kategori
</span>
) : (
<span className="text-slate-300"></span>
)}
</td>
<td className="px-6 py-4 text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
{subCat.trendyol_category_id || '—'}
</td>
<td className="px-6 py-4 text-sm text-slate-400">
{subCat.trendyol_url ? (
<a
href={subCat.trendyol_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
className="text-orange-500 hover:underline inline-flex items-center gap-1"
onClick={(e) => e.stopPropagation()}
>
Trendyol'da
<ExternalLink className="w-3.5 h-3.5" />
</a>
) : (
<span className="text-gray-400">Link yok</span>
<span className="text-slate-300"></span>
)}
</td>
</tr>

View File

@@ -0,0 +1,280 @@
import { useState, useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../config/api'
import { ArrowLeft, ArrowUpRight, ArrowDownRight, Minus, BarChart3 } from 'lucide-react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
function ReportComparison() {
const navigate = useNavigate()
const [reports, setReports] = useState([])
const [loading, setLoading] = useState(true)
const [reportAId, setReportAId] = useState('')
const [reportBId, setReportBId] = useState('')
const [dataA, setDataA] = useState(null)
const [dataB, setDataB] = useState(null)
const [loadingData, setLoadingData] = useState(false)
useEffect(() => {
fetchWithTimeout(`${API_URL}/api/reports`)
.then(res => res.json())
.then(data => setReports(data))
.catch(() => {})
.finally(() => setLoading(false))
}, [])
const loadReport = async (reportId) => {
const res = await fetchWithTimeout(
`${API_URL}/api/reports/${reportId}/dashboard-data`,
{},
TIMEOUT_CONFIG.DASHBOARD
)
if (!res.ok) throw new Error('Rapor yüklenemedi')
return res.json()
}
const handleCompare = async () => {
if (!reportAId || !reportBId) return
setLoadingData(true)
try {
const [a, b] = await Promise.all([loadReport(reportAId), loadReport(reportBId)])
setDataA(a)
setDataB(b)
} catch (err) {
alert('Hata: ' + err.message)
} finally {
setLoadingData(false)
}
}
// Calculate KPIs for a dataset
const calcKpis = (data) => {
if (!data?.all_products) return null
const products = data.all_products
const totalProducts = products.length
const totalOrders = products.reduce((s, p) => s + (p.orders || 0), 0)
const totalViews = products.reduce((s, p) => s + (p.page_views || 0), 0)
const avgPrice = totalProducts > 0
? Math.round(products.reduce((s, p) => s + (p.price || 0), 0) / totalProducts)
: 0
const totalRevenue = products.reduce((s, p) => s + ((p.price || 0) * (p.orders || 0)), 0)
const uniqueBrands = new Set(products.map(p => p.brand).filter(Boolean)).size
// HHI
const brandOrders = {}
products.forEach(p => {
const b = p.brand || 'Unknown'
brandOrders[b] = (brandOrders[b] || 0) + (p.orders || 0)
})
const shares = Object.values(brandOrders).map(o => (o / totalOrders) * 100)
const hhi = Math.round(shares.reduce((s, sh) => s + sh * sh, 0))
return { totalProducts, totalOrders, totalViews, avgPrice, totalRevenue: Math.round(totalRevenue), uniqueBrands, hhi }
}
const kpisA = useMemo(() => calcKpis(dataA), [dataA])
const kpisB = useMemo(() => calcKpis(dataB), [dataB])
// Brand comparison chart
const brandChartData = useMemo(() => {
if (!dataA?.all_products || !dataB?.all_products) return []
const getBrandOrders = (products) => {
const map = {}
products.forEach(p => {
const b = p.brand || 'Bilinmeyen'
map[b] = (map[b] || 0) + (p.orders || 0)
})
return map
}
const brandsA = getBrandOrders(dataA.all_products)
const brandsB = getBrandOrders(dataB.all_products)
// Top 10 brands (by combined orders)
const allBrands = new Set([...Object.keys(brandsA), ...Object.keys(brandsB)])
const combined = Array.from(allBrands).map(name => ({
name,
total: (brandsA[name] || 0) + (brandsB[name] || 0)
}))
combined.sort((a, b) => b.total - a.total)
return combined.slice(0, 10).map(b => ({
name: b.name.length > 15 ? b.name.substring(0, 15) + '...' : b.name,
'Rapor A': brandsA[b.name] || 0,
'Rapor B': brandsB[b.name] || 0
}))
}, [dataA, dataB])
const DiffIndicator = ({ a, b, format = 'number' }) => {
if (a == null || b == null) return <span className="text-slate-400">-</span>
const diff = b - a
const pct = a > 0 ? ((diff / a) * 100).toFixed(1) : '∞'
if (diff === 0) return (
<span className="text-slate-400 flex items-center gap-0.5 text-xs">
<Minus size={12} /> Aynı
</span>
)
if (diff > 0) return (
<span className="text-emerald-600 flex items-center gap-0.5 text-xs font-medium">
<ArrowUpRight size={12} /> +{format === 'currency' ? `${diff.toLocaleString('tr-TR')}` : diff.toLocaleString('tr-TR')} ({pct}%)
</span>
)
return (
<span className="text-red-500 flex items-center gap-0.5 text-xs font-medium">
<ArrowDownRight size={12} /> {format === 'currency' ? `${diff.toLocaleString('tr-TR')}` : diff.toLocaleString('tr-TR')} ({pct}%)
</span>
)
}
const reportAName = reports.find(r => r.id === parseInt(reportAId))?.name || 'Rapor A'
const reportBName = reports.find(r => r.id === parseInt(reportBId))?.name || 'Rapor B'
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">Rapor Karşılaştırma</h1>
<p className="text-sm text-slate-500 mt-1">İki raporu yan yana karşılaştırın</p>
</div>
<button
onClick={() => navigate('/reports')}
className="flex items-center gap-2 px-4 py-2 text-slate-500 hover:text-slate-700 hover:bg-slate-50 rounded-lg transition-colors"
>
<ArrowLeft size={18} />
Geri
</button>
</div>
</div>
{/* Report Selectors */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Rapor A</label>
<select
value={reportAId}
onChange={e => setReportAId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
>
<option value="">Rapor seçin...</option>
{reports.map(r => (
<option key={r.id} value={r.id} disabled={r.id === parseInt(reportBId)}>
{r.name} ({r.total_products} ürün)
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Rapor B</label>
<select
value={reportBId}
onChange={e => setReportBId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
>
<option value="">Rapor seçin...</option>
{reports.map(r => (
<option key={r.id} value={r.id} disabled={r.id === parseInt(reportAId)}>
{r.name} ({r.total_products} ürün)
</option>
))}
</select>
</div>
</div>
<button
onClick={handleCompare}
disabled={!reportAId || !reportBId || loadingData}
className="w-full md:w-auto px-6 py-2.5 bg-orange-500 text-white text-sm rounded-lg font-medium hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loadingData ? 'Yükleniyor...' : 'Karşılaştır'}
</button>
</div>
{/* Comparison Results */}
{kpisA && kpisB && (
<>
{/* KPI Comparison Table */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h3 className="text-lg font-semibold text-slate-900">KPI Karşılaştırma</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
<th className="text-left px-6 py-3 font-medium text-slate-500">Metrik</th>
<th className="text-right px-6 py-3 font-medium text-orange-500">{reportAName}</th>
<th className="text-right px-6 py-3 font-medium text-blue-500">{reportBName}</th>
<th className="text-right px-6 py-3 font-medium text-slate-500">Fark</th>
</tr>
</thead>
<tbody>
{[
{ label: 'Toplam Ürün', keyA: kpisA.totalProducts, keyB: kpisB.totalProducts },
{ label: 'Toplam Sipariş', keyA: kpisA.totalOrders, keyB: kpisB.totalOrders },
{ label: 'Toplam Görüntülenme', keyA: kpisA.totalViews, keyB: kpisB.totalViews },
{ label: 'Ortalama Fiyat', keyA: kpisA.avgPrice, keyB: kpisB.avgPrice, format: 'currency' },
{ label: 'Toplam Ciro', keyA: kpisA.totalRevenue, keyB: kpisB.totalRevenue, format: 'currency' },
{ label: 'Marka Sayısı', keyA: kpisA.uniqueBrands, keyB: kpisB.uniqueBrands },
{ label: 'HHI (Yoğunlaşma)', keyA: kpisA.hhi, keyB: kpisB.hhi },
].map((row, i) => (
<tr key={i} className="border-b border-slate-50 hover:bg-slate-50/50">
<td className="px-6 py-3 font-medium text-slate-700">{row.label}</td>
<td className="px-6 py-3 text-right text-slate-900">
{row.format === 'currency' ? `${row.keyA.toLocaleString('tr-TR')}` : row.keyA.toLocaleString('tr-TR')}
</td>
<td className="px-6 py-3 text-right text-slate-900">
{row.format === 'currency' ? `${row.keyB.toLocaleString('tr-TR')}` : row.keyB.toLocaleString('tr-TR')}
</td>
<td className="px-6 py-3 text-right">
<DiffIndicator a={row.keyA} b={row.keyB} format={row.format} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Brand Comparison Chart */}
{brandChartData.length > 0 && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">
<BarChart3 size={18} className="inline mr-2" />
En Çok Satan 10 Marka Karşılaştırması
</h3>
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={brandChartData} margin={{ top: 5, right: 30, left: 20, bottom: 60 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis
dataKey="name"
tick={{ fill: '#64748b', fontSize: 11 }}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis tick={{ fill: '#64748b', fontSize: 12 }} />
<Tooltip
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0' }}
formatter={(value) => [value.toLocaleString('tr-TR'), '']}
/>
<Legend />
<Bar dataKey="Rapor A" fill="#f97316" radius={[4, 4, 0, 0]} />
<Bar dataKey="Rapor B" fill="#3b82f6" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
</>
)}
</div>
)
}
export default ReportComparison

View File

@@ -3,6 +3,8 @@ import { useParams, useNavigate } from 'react-router-dom'
import { TAB_GROUPS, ALL_TABS } from '../constants/tabGroups'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../config/api'
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend, ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, ZAxis, BarChart, Bar } from 'recharts'
import { ArrowLeft, BarChart3, Award, Grid3X3, Globe, Barcode, Key, Search, Trophy, Target, Download, Printer } from 'lucide-react'
import { PageSkeleton } from './ui/SkeletonLoader'
import BarcodeTab from './dashboard-tabs/BarcodeTab'
import OriginTab from './dashboard-tabs/OriginTab'
import OverviewTab from './dashboard-tabs/OverviewTab'
@@ -10,6 +12,9 @@ import BrandTab from './dashboard-tabs/BrandTab'
import CategoryTab from './dashboard-tabs/CategoryTab'
import KeywordTab from './dashboard-tabs/KeywordTab'
import ProductFinderTab from './dashboard-tabs/ProductFinderTab'
import HiddenChampionsTab from './dashboard-tabs/HiddenChampionsTab'
import OpportunityTab from './dashboard-tabs/OpportunityTab'
import { exportToExcel, printReport } from '../utils/exportUtils'
function ReportDashboard() {
const { reportId } = useParams()
@@ -1126,14 +1131,7 @@ function ReportDashboard() {
}, [dashboardData])
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-600">Dashboard yükleniyor...</p>
</div>
</div>
)
return <PageSkeleton />
}
if (error) {
@@ -1141,11 +1139,11 @@ function ReportDashboard() {
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="text-red-500 text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">Hata</h2>
<p className="text-gray-600 mb-4">{error}</p>
<h2 className="text-2xl font-bold text-slate-800 mb-2">Hata</h2>
<p className="text-slate-500 mb-4">{error}</p>
<button
onClick={() => navigate('/reports')}
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
className="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600"
>
Raporlara Dön
</button>
@@ -1155,40 +1153,74 @@ function ReportDashboard() {
}
return (
<div className="min-h-screen bg-gray-100">
<div className="min-h-screen bg-slate-100">
<div className="w-full px-4 py-6">
{/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">{dashboardData.report_name}</h1>
<p className="text-gray-600 mt-1">{dashboardData.category_name}</p>
<h1 className="text-3xl font-bold text-slate-900">{dashboardData.report_name}</h1>
<p className="text-slate-500 mt-1">{dashboardData.category_name}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => exportToExcel(dashboardData, dashboardData.report_name)}
className="flex items-center gap-2 px-4 py-2 text-emerald-600 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors text-sm font-medium"
title="Excel İndir"
>
<Download size={16} />
Excel
</button>
<button
onClick={printReport}
className="flex items-center gap-2 px-4 py-2 text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors text-sm font-medium"
title="Yazdır"
>
<Printer size={16} />
Yazdır
</button>
<button
onClick={() => navigate('/reports')}
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
className="flex items-center gap-2 px-4 py-2 text-slate-500 hover:text-slate-700 hover:bg-orange-50/30 rounded-lg transition-colors"
>
Geri
<ArrowLeft size={18} />
Geri
</button>
</div>
</div>
</div>
{/* Tab Navigation */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 mb-6">
<div className="flex border-b border-gray-200 overflow-x-auto">
{ALL_TABS.map(tab => (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 mb-6 p-3">
<div className="flex gap-2 overflow-x-auto">
{ALL_TABS.map(tab => {
const TAB_ICONS = {
'overview': BarChart3,
'brand': Award,
'category': Grid3X3,
'origin': Globe,
'barcode': Barcode,
'keyword': Key,
'product-finder': Search,
'hidden-champions': Trophy,
'opportunity': Target
}
const TabIcon = TAB_ICONS[tab.id] || BarChart3
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-6 py-4 font-medium whitespace-nowrap transition-colors ${
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${
activeTab === tab.id
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
? 'bg-orange-500 text-white shadow-sm'
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100'
}`}
>
<TabIcon size={16} />
{tab.name}
</button>
))}
)
})}
</div>
</div>
@@ -1202,6 +1234,8 @@ function ReportDashboard() {
topSellingBrands={topSellingBrands}
topSellingCategories={topSellingCategories}
mostViewedCategories={mostViewedCategories}
reportId={reportId}
allProducts={dashboardData?.all_products || []}
/>
)}
@@ -1240,6 +1274,16 @@ function ReportDashboard() {
{activeTab === 'product-finder' && (
<ProductFinderTab allProducts={dashboardData.all_products || []} />
)}
{/* GİZLİ ŞAMPİYONLAR TAB */}
{activeTab === 'hidden-champions' && (
<HiddenChampionsTab reportId={reportId} />
)}
{/* FIRSAT HARİTASI TAB */}
{activeTab === 'opportunity' && (
<OpportunityTab allProducts={dashboardData?.all_products || []} />
)}
</div>
</div>
</div>

View File

@@ -1,11 +1,12 @@
import { useState, useEffect, useRef } from 'react'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG, calculateNextDelay } from '../config/api'
import { Search, Loader2, ChevronRight, ArrowLeft, X, Check } from 'lucide-react'
function ReportGeneration() {
const [mainCategories, setMainCategories] = useState([])
const [selectedCategory, setSelectedCategory] = useState(null)
const [subCategories, setSubCategories] = useState([])
const [selectedSubCategories, setSelectedSubCategories] = useState([]) // Changed to array for multi-select
const [selectedSubCategories, setSelectedSubCategories] = useState([])
const [loadingSubCategories, setLoadingSubCategories] = useState(false)
const [subCategorySearch, setSubCategorySearch] = useState('')
const [loading, setLoading] = useState(false)
@@ -18,6 +19,8 @@ function ReportGeneration() {
const [error, setError] = useState(null)
const [showCompletionModal, setShowCompletionModal] = useState(false)
const [completionData, setCompletionData] = useState(null)
const [subBreadcrumb, setSubBreadcrumb] = useState([])
const [showSearch, setShowSearch] = useState(false)
const pollTimeoutRef = useRef(null)
const isMountedRef = useRef(true)
const logsEndRef = useRef(null)
@@ -34,7 +37,6 @@ function ReportGeneration() {
}
}, [])
// Auto-scroll to bottom of logs
useEffect(() => {
if (logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
@@ -62,11 +64,15 @@ function ReportGeneration() {
}
}
const fetchSubCategories = async (categoryId) => {
const fetchSubCategories = async (categoryId, resetSelection = true) => {
setLoadingSubCategories(true)
setSubCategories([])
setSelectedSubCategories([]) // Clear selected subcategories
setSubCategorySearch('') // Clear search when loading new category
if (resetSelection) {
setSelectedSubCategories([])
setSubBreadcrumb([])
}
setSubCategorySearch('')
setShowSearch(false)
try {
const response = await fetchWithTimeout(`${API_URL}/categories/${categoryId}/children`)
if (!response.ok) throw new Error('Failed to fetch sub-categories')
@@ -85,6 +91,32 @@ function ReportGeneration() {
}
}
const handleDrillDown = (subCat) => {
if (generating || subCat.children_count === 0) return
setSubBreadcrumb(prev => [...prev, subCat])
fetchSubCategories(subCat.id, false)
}
const handleBreadcrumbBack = (index) => {
if (index === -1) {
setSubBreadcrumb([])
fetchSubCategories(selectedCategory.id, false)
} else {
const target = subBreadcrumb[index]
setSubBreadcrumb(subBreadcrumb.slice(0, index + 1))
fetchSubCategories(target.id, false)
}
}
const toggleSubCategory = (subCat) => {
if (generating) return
setSelectedSubCategories(prev => {
const exists = prev.some(c => c.id === subCat.id)
if (exists) return prev.filter(c => c.id !== subCat.id)
return [...prev, subCat]
})
}
const addLog = (message, type = 'info') => {
const timestamp = new Date().toLocaleTimeString('tr-TR')
setLogs((prev) => [...prev, { timestamp, message, type }])
@@ -95,8 +127,6 @@ function ReportGeneration() {
alert('Lütfen önce bir kategori seçin!')
return
}
// Show modal first to get report name
setReportName(`${new Date().toLocaleDateString('tr-TR', { month: 'long' })} ${selectedCategory.name} Raporu`)
setShowNameModal(true)
}
@@ -113,13 +143,11 @@ function ReportGeneration() {
setLogs([])
try {
// Prepare request body
const requestBody = {
name: reportName,
category_id: selectedCategory.id
}
// Add subcategory_ids if subcategories are selected
if (selectedSubCategories.length > 0) {
requestBody.subcategory_ids = selectedSubCategories.map(cat => cat.id)
}
@@ -129,10 +157,6 @@ function ReportGeneration() {
console.log(' - Kategori ID:', requestBody.category_id)
console.log(' - Alt kategori IDs:', requestBody.subcategory_ids)
// Build URL with query parameters for POST request with streaming
const url = new URL(`${API_URL}/api/reports/create`)
// Build SSE URL
const params = new URLSearchParams({
name: requestBody.name,
category_id: requestBody.category_id,
@@ -142,7 +166,6 @@ function ReportGeneration() {
const sseUrl = `${API_URL}/api/reports/create?${params}`
console.log('🌐 SSE URL:', sseUrl)
// Start SSE connection
const eventSource = new EventSource(sseUrl)
console.log('📡 EventSource oluşturuldu')
@@ -155,12 +178,10 @@ function ReportGeneration() {
try {
const data = JSON.parse(event.data)
// Update progress
if (data.progress !== undefined) {
setProgress(data.progress)
}
// Add log message
if (data.message) {
const timestamp = new Date().toLocaleTimeString('tr-TR')
setLogs(prev => [...prev, {
@@ -170,23 +191,17 @@ function ReportGeneration() {
}])
}
// Handle completion
if (data.type === 'complete') {
eventSource.close()
// Store completion data
setCompletionData({
report_id: data.report_id,
total_products: data.total_products,
successful: data.successful
})
// Show completion modal
setShowCompletionModal(true)
setGenerating(false)
}
// Handle error
if (data.type === 'error' && data.progress === -1) {
eventSource.close()
setGenerating(false)
@@ -199,8 +214,6 @@ function ReportGeneration() {
eventSource.onerror = (error) => {
console.error('❌ SSE Error:', error)
console.error('EventSource readyState:', eventSource.readyState)
console.error('EventSource url:', eventSource.url)
eventSource.close()
addLog('Bağlantı hatası oluştu', 'error')
setGenerating(false)
@@ -214,7 +227,6 @@ function ReportGeneration() {
const handleViewReport = () => {
if (completionData && completionData.report_id) {
// Navigate to report dashboard
window.location.href = `/reports/${completionData.report_id}`
}
}
@@ -222,7 +234,10 @@ function ReportGeneration() {
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Yükleniyor...</div>
<div className="text-slate-400 flex items-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
Yükleniyor...
</div>
</div>
)
}
@@ -235,35 +250,36 @@ function ReportGeneration() {
)
}
return (
<div className="space-y-6">
{/* Info Card */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 border-l-4 border-l-blue-600">
<div className="flex items-start">
<div className="ml-3">
<h3 className="text-sm font-semibold text-gray-900">Rapor Oluşturma Süreci</h3>
<div className="mt-2 text-sm text-gray-600">
<ol className="list-decimal list-inside space-y-1">
<li>Ana kategoriyi seçin</li>
<li>(Opsiyonel) Sadece bir alt kategori seçebilirsiniz</li>
<li>"Rapor Oluştur" butonuna tıklayın</li>
<li>Sistem seçili kategorilerden verileri çekecek</li>
<li>Rapora bir ad verin ve kaydedin</li>
<li>"Raporlarım" sekmesinden raporlarınızı görüntüleyin</li>
</ol>
</div>
</div>
</div>
</div>
const filteredSubCategories = subCategories.filter((subCat) =>
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
)
{/* Category Selection */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
1. Kategori Seçin
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
return (
<div className="space-y-5">
{/* Main Category Selection - Horizontal Pills */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider">Kategori</h2>
{selectedCategory && (
<button
onClick={() => {
if (!generating) {
setSelectedCategory(null)
setSubCategories([])
setSelectedSubCategories([])
setSubBreadcrumb([])
}
}}
disabled={generating}
className="text-xs text-slate-400 hover:text-slate-600 transition-colors"
>
Temizle
</button>
)}
</div>
<div className="flex flex-wrap gap-2">
{mainCategories.map((category) => (
<div
<button
key={category.id}
onClick={() => {
if (!generating) {
@@ -271,263 +287,290 @@ function ReportGeneration() {
fetchSubCategories(category.id)
}
}}
className={`bg-white border-l-4 ${selectedCategory?.id === category.id ? 'border-blue-600' : 'border-gray-300'} rounded-lg shadow-sm p-4 cursor-pointer hover:shadow-md transition-shadow ${
selectedCategory?.id === category.id ? 'ring-2 ring-blue-200' : ''
disabled={generating}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
selectedCategory?.id === category.id
? 'bg-orange-500 text-white shadow-sm shadow-orange-200'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-800'
} ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div className="flex flex-col space-y-2">
<h3 className="text-sm font-semibold text-gray-900">{category.name}</h3>
<p className="text-xs text-gray-600">
{category.children_count || 0} alt kategori
</p>
</div>
</div>
{category.name}
</button>
))}
</div>
</div>
{/* Sub-Category Selection (Optional) */}
{selectedCategory && subCategories.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
2. Alt Kategori Seçin (Opsiyonel)
</h2>
<div className="space-y-4">
<p className="text-sm text-gray-600">
İsterseniz sadece bir alt kategori için rapor oluşturabilirsiniz. Seçmezseniz tüm alt kategoriler için rapor oluşturulur.
</p>
{loadingSubCategories ? (
<div className="text-center text-gray-500">Alt kategoriler yükleniyor...</div>
{/* Sub-Category Selection - Clean List */}
{selectedCategory && (subCategories.length > 0 || loadingSubCategories) && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
{/* Header */}
<div className="px-5 py-4 border-b border-slate-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
{subBreadcrumb.length > 0 && (
<button
onClick={() => !generating && handleBreadcrumbBack(subBreadcrumb.length >= 2 ? subBreadcrumb.length - 2 : -1)}
disabled={generating}
className="w-7 h-7 flex items-center justify-center rounded-md bg-slate-100 hover:bg-slate-200 text-slate-400 hover:text-slate-600 transition-colors"
>
<ArrowLeft className="w-3.5 h-3.5" />
</button>
)}
<div className="flex items-center gap-1.5 text-sm">
<button
onClick={() => !generating && subBreadcrumb.length > 0 && handleBreadcrumbBack(-1)}
className={`font-medium transition-colors ${subBreadcrumb.length > 0 ? 'text-orange-500 hover:text-orange-600 cursor-pointer' : 'text-slate-800 cursor-default'}`}
disabled={generating || subBreadcrumb.length === 0}
>
{selectedCategory.name}
</button>
{subBreadcrumb.map((crumb, index) => (
<span key={crumb.id} className="flex items-center gap-1.5">
<ChevronRight className="w-3 h-3 text-slate-300" />
{index < subBreadcrumb.length - 1 ? (
<button
onClick={() => !generating && handleBreadcrumbBack(index)}
className="text-orange-500 hover:text-orange-600 font-medium"
disabled={generating}
>
{crumb.name}
</button>
) : (
<div className="space-y-2">
{/* Search Bar */}
<div className="relative">
<span className="text-slate-800 font-medium">{crumb.name}</span>
)}
</span>
))}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowSearch(!showSearch)}
className={`w-7 h-7 flex items-center justify-center rounded-md transition-colors ${
showSearch ? 'bg-orange-100 text-orange-500' : 'bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600'
}`}
>
<Search className="w-3.5 h-3.5" />
</button>
<span className="text-xs text-slate-400 tabular-nums">{subCategories.length}</span>
</div>
</div>
{/* Search - collapsible */}
{showSearch && (
<div className="relative mt-3">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-300" />
<input
type="text"
placeholder="Alt kategori ara... (örn: Telefon)"
placeholder="Ara..."
value={subCategorySearch}
onChange={(e) => setSubCategorySearch(e.target.value)}
disabled={generating}
className={`w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:border-blue-600 transition-colors ${
generating ? 'opacity-50 cursor-not-allowed bg-gray-100' : 'bg-white'
}`}
autoFocus
className="w-full pl-9 pr-8 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-orange-300 focus:ring-1 focus:ring-orange-300 transition-all"
/>
{subCategorySearch && (
<button
onClick={() => setSubCategorySearch('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-300 hover:text-slate-500"
>
×
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
{/* Selection info and controls */}
<div className="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-700">
{selectedSubCategories.length === 0
? 'Tüm Alt Kategoriler Seçili'
: `${selectedSubCategories.length} Kategori Seçildi`}
</span>
)}
</div>
<div className="flex gap-2">
{/* Selected chips */}
{selectedSubCategories.length > 0 && (
<div className="px-5 py-3 border-b border-slate-100 bg-orange-50/40">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[11px] font-semibold text-orange-500 uppercase tracking-wider shrink-0">
{selectedSubCategories.length} seçili
</span>
<div className="w-px h-4 bg-orange-200 shrink-0" />
{selectedSubCategories.map((cat) => (
<span
key={cat.id}
className="inline-flex items-center gap-1 px-2.5 py-1 bg-white rounded-full text-xs font-medium text-slate-700 border border-orange-200 shadow-sm"
>
{cat.name}
<button
onClick={() => !generating && setSelectedSubCategories(prev => prev.filter(c => c.id !== cat.id))}
disabled={generating}
className="text-slate-300 hover:text-red-400 transition-colors ml-0.5"
>
<X className="w-3 h-3" />
</button>
</span>
))}
<button
onClick={() => !generating && setSelectedSubCategories([])}
disabled={generating}
className={`px-3 py-1 text-sm rounded-lg border-2 transition-all ${
generating
? 'opacity-50 cursor-not-allowed'
: 'border-red-300 text-red-700 hover:bg-red-50'
}`}
className="text-[11px] text-slate-400 hover:text-red-400 font-medium transition-colors ml-auto shrink-0"
>
Seçimi Temizle
Temizle
</button>
</div>
</div>
)}
<button
{/* List */}
{loadingSubCategories ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-5 h-5 animate-spin text-orange-400" />
</div>
) : (
<div className="max-h-[420px] overflow-y-auto">
{/* Select all row */}
<div
onClick={() => {
if (!generating) {
const filteredSubs = subCategories.filter((subCat) =>
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
)
setSelectedSubCategories(filteredSubs)
if (generating) return
const allIds = new Set(selectedSubCategories.map(c => c.id))
const allSelected = filteredSubCategories.every(c => allIds.has(c.id))
if (allSelected) {
const filterIds = new Set(filteredSubCategories.map(c => c.id))
setSelectedSubCategories(prev => prev.filter(c => !filterIds.has(c.id)))
} else {
const newOnes = filteredSubCategories.filter(c => !allIds.has(c.id))
setSelectedSubCategories(prev => [...prev, ...newOnes])
}
}}
disabled={generating}
className={`px-3 py-1 text-sm rounded-lg border-2 transition-all ${
generating
? 'opacity-50 cursor-not-allowed'
: 'border-green-600 text-green-700 hover:bg-green-50'
}`}
className={`flex items-center px-5 py-2.5 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{subCategorySearch ? 'Gösterilenleri Seç' : 'Tümünü Seç'}
</button>
<div className={`w-[18px] h-[18px] rounded border-2 flex items-center justify-center mr-3 shrink-0 transition-colors ${
filteredSubCategories.length > 0 && filteredSubCategories.every(c => selectedSubCategories.some(s => s.id === c.id))
? 'bg-orange-500 border-orange-500'
: 'border-slate-300'
}`}>
{filteredSubCategories.length > 0 && filteredSubCategories.every(c => selectedSubCategories.some(s => s.id === c.id)) && (
<Check className="w-3 h-3 text-white" strokeWidth={3} />
)}
</div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Tümünü seç</span>
</div>
{/* Sub-category options */}
{(() => {
const filteredSubCategories = subCategories.filter((subCat) =>
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
)
return filteredSubCategories.length > 0 ? (
<div className="max-h-64 overflow-y-auto space-y-2 border rounded-lg p-2">
{filteredSubCategories.map((subCat) => {
const isSelected = selectedSubCategories.some(cat => cat.id === subCat.id)
{filteredSubCategories.length > 0 ? (
filteredSubCategories.map((subCat) => {
const isSelected = selectedSubCategories.some(c => c.id === subCat.id)
const hasChildren = subCat.children_count > 0
return (
<div
key={subCat.id}
onClick={() => {
if (!generating) {
if (isSelected) {
// Remove from selection
setSelectedSubCategories(prev =>
prev.filter(cat => cat.id !== subCat.id)
)
} else {
// Add to selection
setSelectedSubCategories(prev => [...prev, subCat])
}
}
}}
className={`p-3 rounded-lg border-2 cursor-pointer transition-all ${
isSelected
? 'border-blue-600 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
className={`group flex items-center px-5 py-3 border-b border-slate-50 transition-colors ${
isSelected ? 'bg-orange-50/60' : 'hover:bg-slate-50/80'
} ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected}
onChange={() => {}} // Handled by parent div onClick
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-600 cursor-pointer"
{/* Checkbox */}
<button
onClick={() => toggleSubCategory(subCat)}
disabled={generating}
/>
<span className="text-sm font-medium text-gray-900">{subCat.name}</span>
</div>
className="mr-3 shrink-0"
>
<div className={`w-[18px] h-[18px] rounded border-2 flex items-center justify-center transition-all duration-150 ${
isSelected
? 'bg-orange-500 border-orange-500 scale-105'
: 'border-slate-300 group-hover:border-slate-400'
}`}>
{isSelected && <Check className="w-3 h-3 text-white" strokeWidth={3} />}
</div>
</button>
{/* Name - click to select */}
<button
onClick={() => toggleSubCategory(subCat)}
disabled={generating}
className="flex-1 text-left min-w-0"
>
<span className={`text-sm font-medium ${isSelected ? 'text-orange-900' : 'text-slate-700'}`}>
{subCat.name}
</span>
</button>
{/* Children count + drill down */}
{hasChildren && (
<button
onClick={(e) => {
e.stopPropagation()
handleDrillDown(subCat)
}}
disabled={generating}
className="group/drill flex items-center gap-1.5 ml-3 px-2.5 py-1.5 rounded-lg bg-slate-100 text-slate-500 hover:text-orange-600 hover:bg-orange-100 transition-all shrink-0 border border-transparent hover:border-orange-200"
title="Alt kategorileri gör"
>
<span className="text-xs tabular-nums font-medium">{subCat.children_count}</span>
<ChevronRight className="w-3.5 h-3.5 transition-transform group-hover/drill:translate-x-0.5" />
</button>
)}
</div>
)
})}
</div>
})
) : (
<div className="text-center py-8 text-gray-500 border rounded-lg bg-gray-50">
<p className="font-medium">Sonuç bulunamadı</p>
<p className="text-sm mt-1">"{subCategorySearch}" araması için kategori bulunamadı</p>
</div>
)
})()}
<div className="text-center py-12 text-slate-400">
<p className="text-sm">Sonuç bulunamadı</p>
</div>
)}
</div>
)}
</div>
)}
{/* Generate Button */}
{selectedCategory && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
{subCategories.length > 0 ? '3. Rapor Oluştur' : '2. Rapor Oluştur'}
</h2>
<div className="flex flex-col items-center justify-center py-4">
<p className="text-gray-600 mb-4 text-center">
<strong>{selectedCategory.name}</strong> kategorisi için{' '}
{/* Generate Button - compact inline */}
{selectedCategory && !generating && (
<div className="flex items-center justify-between bg-white rounded-xl shadow-sm border border-slate-200 px-5 py-4">
<p className="text-sm text-slate-500">
<span className="font-semibold text-slate-800">{selectedCategory.name}</span>
<span className="mx-1.5 text-slate-300">·</span>
{selectedSubCategories.length > 0 ? (
<>
<strong>{selectedSubCategories.length}</strong> seçili alt kategoriden veri çekilecek
</>
<span>{selectedSubCategories.length} seçili kategori</span>
) : (
<>
<strong>{selectedCategory.children_count}</strong> alt kategoriden veri çekilecek
</>
<span>{selectedCategory.children_count || 0} alt kategori</span>
)}
</p>
<button
onClick={handleGenerateReport}
disabled={generating}
className={`px-8 py-4 rounded-lg font-medium text-lg transition-all ${
generating
? 'bg-gray-300 text-gray-600 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md hover:shadow-lg'
}`}
className="px-6 py-2.5 bg-orange-500 text-white text-sm rounded-lg font-medium hover:bg-orange-600 shadow-sm hover:shadow transition-all"
>
{generating ? (
<span className="flex items-center">
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Rapor Oluşturuluyor... {progress}%
</span>
) : (
<>Rapor Oluştur</>
)}
Rapor Oluştur
</button>
</div>
)}
{/* Progress Bar */}
{/* Progress - shows during generation */}
{generating && (
<div className="w-full max-w-md mt-6">
<div className="w-full bg-gray-200 rounded-full h-3">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 px-5 py-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-slate-700">Rapor oluşturuluyor...</span>
<span className="text-sm font-semibold text-orange-500 tabular-nums">{progress}%</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-blue-600 h-3 rounded-full transition-all duration-500"
className="bg-orange-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
</div>
</div>
)}
{/* Terminal Logs */}
{logs.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<span>Terminal - Rapor Oluşturuluyor</span>
</h3>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
<span className="text-sm text-gray-600">Canlı</span>
</div>
</div>
<div className="bg-gray-900 rounded-lg p-4 max-h-96 overflow-y-auto border-2 border-gray-700">
<div className="bg-slate-900 rounded-xl overflow-hidden border border-slate-700">
{/* Terminal Header */}
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-700">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-slate-700/50">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<div className="w-2.5 h-2.5 rounded-full bg-red-500/80"></div>
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/80"></div>
<div className="w-2.5 h-2.5 rounded-full bg-green-500/80"></div>
</div>
<span className="text-gray-400 text-xs font-mono ml-2">trendyol-analytics-terminal</span>
<span className="text-slate-500 text-xs font-mono ml-2">terminal</span>
{generating && <div className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse ml-auto"></div>}
</div>
{/* Terminal Content */}
<div className="p-4 max-h-80 overflow-y-auto">
{logs.map((log, index) => (
<div
key={index}
className={`font-mono text-sm mb-2 ${
className={`font-mono text-xs mb-1.5 ${
log.type === 'error'
? 'text-red-400'
: log.type === 'success'
@@ -538,14 +581,12 @@ function ReportGeneration() {
? 'text-blue-400'
: log.type === 'processing'
? 'text-cyan-400'
: 'text-gray-300'
: 'text-slate-400'
}`}
>
<span className="text-gray-500">[{log.timestamp}]</span> {log.message}
<span className="text-slate-600">[{log.timestamp}]</span> {log.message}
</div>
))}
{/* Auto-scroll anchor */}
<div ref={logsEndRef} />
</div>
</div>
@@ -553,74 +594,68 @@ function ReportGeneration() {
{/* Name Modal */}
{showNameModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8 max-w-md w-full mx-4">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Rapora Ad Verin</h2>
<p className="text-gray-600 mb-6">
Rapor başarıyla oluşturuldu! Kaydetmek için bir isim verin.
</p>
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl">
<h2 className="text-lg font-bold text-slate-900 mb-1">Rapor Adı</h2>
<p className="text-sm text-slate-400 mb-4">Raporunuz için bir isim belirleyin</p>
<input
type="text"
value={reportName}
onChange={(e) => setReportName(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-6"
placeholder="Örn: Kasım Ayı Kozmetik Raporu"
className="w-full px-4 py-2.5 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent mb-4"
placeholder="Örn: Kasım Kozmetik Raporu"
autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleStartReportGeneration()}
/>
<div className="flex space-x-4">
<div className="flex gap-3">
<button
onClick={() => {
setShowNameModal(false)
setGenerating(false)
}}
className="flex-1 px-4 py-3 bg-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-400 transition-colors"
className="flex-1 px-4 py-2.5 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 transition-colors"
>
İptal
</button>
<button
onClick={handleStartReportGeneration}
className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
className="flex-1 px-4 py-2.5 bg-orange-500 text-white rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors"
>
Rapor Oluştur
Başla
</button>
</div>
</div>
</div>
)}
{/* Completion Modal - Shows when report is 100% complete */}
{/* Completion Modal */}
{showCompletionModal && completionData && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-8 max-w-md w-full shadow-2xl">
<div className="text-center">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-12 h-12 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl text-center">
<div className="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Check className="w-7 h-7 text-green-600" strokeWidth={2.5} />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">Rapor Tamamlandı!</h3>
<p className="text-gray-600 mb-6">
<strong>{reportName}</strong> başarıyla oluşturuldu.
</p>
<div className="bg-gray-50 rounded-lg p-4 mb-6 border border-gray-200">
<div className="flex justify-between items-center mb-2">
<span className="text-gray-600">Toplam Ürün:</span>
<span className="font-bold text-gray-900">{completionData.total_products}</span>
<h3 className="text-lg font-bold text-slate-900 mb-1">Tamamlandı</h3>
<p className="text-sm text-slate-400 mb-5">{reportName}</p>
<div className="flex gap-4 justify-center mb-5 text-center">
<div>
<p className="text-2xl font-bold text-slate-900">{completionData.total_products}</p>
<p className="text-xs text-slate-400">Ürün</p>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Başarılı Kategori:</span>
<span className="font-bold text-green-600">{completionData.successful}</span>
<div className="w-px bg-slate-200" />
<div>
<p className="text-2xl font-bold text-green-600">{completionData.successful}</p>
<p className="text-xs text-slate-400">Kategori</p>
</div>
</div>
<button
onClick={handleViewReport}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
className="w-full px-6 py-2.5 bg-orange-500 text-white rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors"
>
Raporu Görüntüle
</button>
</div>
</div>
</div>
)}
</div>
)

View File

@@ -1,11 +1,14 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { API_URL, fetchWithTimeout } from '../config/api'
import { Link, useNavigate } from 'react-router-dom'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG, POLLING_CONFIG, calculateNextDelay } from '../config/api'
import { FileBarChart, Trash2, Eye, Calendar, Layers, Package, Sparkles, GitCompareArrows } from 'lucide-react'
function ReportList() {
const navigate = useNavigate()
const [reports, setReports] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [enrichingReports, setEnrichingReports] = useState({}) // { reportId: { status, progress, enriched } }
useEffect(() => {
fetchReports()
@@ -19,6 +22,16 @@ function ReportList() {
if (!response.ok) throw new Error('Failed to fetch reports')
const data = await response.json()
setReports(data)
// Check enrichment status for each report
data.forEach(report => {
if (report.is_enriched) {
setEnrichingReports(prev => ({
...prev,
[report.id]: { status: 'completed', progress: 100, enriched: true }
}))
}
})
} catch (err) {
setError(err.message)
} finally {
@@ -46,6 +59,74 @@ function ReportList() {
}
}
const handleEnrich = async (reportId) => {
// Set loading state
setEnrichingReports(prev => ({
...prev,
[reportId]: { status: 'loading', progress: 0, enriched: false }
}))
try {
// Start enrichment
const response = await fetchWithTimeout(
`${API_URL}/api/reports/${reportId}/enrich/start`,
{ method: 'POST' },
TIMEOUT_CONFIG.ENRICHMENT
)
if (!response.ok) {
const errData = await response.json().catch(() => ({}))
throw new Error(errData.detail || 'Zenginleştirme başlatılamadı')
}
// Start polling for status
pollEnrichmentStatus(reportId)
} catch (err) {
setEnrichingReports(prev => ({
...prev,
[reportId]: { status: 'error', progress: 0, enriched: false, error: err.message }
}))
}
}
const pollEnrichmentStatus = async (reportId) => {
let delay = POLLING_CONFIG.INITIAL_DELAY
const poll = async () => {
try {
const response = await fetchWithTimeout(
`${API_URL}/api/reports/${reportId}/enrich/status`
)
if (!response.ok) throw new Error('Status check failed')
const data = await response.json()
const progress = data.progress || 0
const isComplete = data.status === 'completed' || progress >= 100
setEnrichingReports(prev => ({
...prev,
[reportId]: {
status: isComplete ? 'completed' : 'loading',
progress,
enriched: isComplete
}
}))
if (!isComplete) {
delay = calculateNextDelay(delay)
setTimeout(poll, delay)
}
} catch {
// On error, retry a few times
delay = calculateNextDelay(delay)
setTimeout(poll, delay)
}
}
setTimeout(poll, delay)
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('tr-TR', {
@@ -60,14 +141,14 @@ function ReportList() {
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Yükleniyor...</div>
<div className="text-slate-400">Yükleniyor...</div>
</div>
)
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
<p className="text-red-700">Hata: {error}</p>
</div>
)
@@ -76,8 +157,8 @@ function ReportList() {
if (reports.length === 0) {
return (
<div className="text-center py-12">
<h3 className="text-xl font-semibold text-gray-800 mb-2">Henüz Rapor Yok</h3>
<p className="text-gray-600 mb-6">
<h3 className="text-xl font-semibold text-slate-800 mb-2">Henüz Rapor Yok</h3>
<p className="text-slate-500 mb-6">
"Rapor Oluştur" sekmesinden ilk raporunuzu oluşturun
</p>
</div>
@@ -87,70 +168,123 @@ function ReportList() {
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900">Raporlarım</h1>
<p className="text-sm text-gray-600 mt-1">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">Raporlarım</h1>
<p className="text-sm text-slate-500 mt-1">
Toplam {reports.length} rapor
</p>
</div>
{reports.length >= 2 && (
<button
onClick={() => navigate('/compare')}
className="flex items-center gap-2 px-4 py-2 text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors text-sm font-medium"
>
<GitCompareArrows size={16} />
Karşılaştır
</button>
)}
</div>
</div>
{/* Reports Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{reports.map((report) => (
{reports.map((report) => {
const enrichState = enrichingReports[report.id]
const isEnriched = enrichState?.enriched || report.is_enriched
const isEnriching = enrichState?.status === 'loading'
const enrichProgress = enrichState?.progress || 0
return (
<div
key={report.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 border-l-4 border-l-blue-600 p-6 hover:shadow-md transition-shadow"
className="bg-white rounded-xl shadow-sm border border-slate-200 border-l-4 border-l-orange-500 p-6 hover:shadow-md transition-shadow"
>
{/* Report Header */}
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-semibold text-gray-900 text-base">
<h3 className="font-semibold text-slate-900 text-base">
{report.name}
</h3>
<p className="text-sm text-gray-600 mt-1">
<p className="text-sm text-slate-500 mt-1">
{report.category_name}
</p>
</div>
{isEnriched && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-100 text-emerald-700 text-xs font-medium rounded-full">
<Sparkles size={10} />
Zengin
</span>
)}
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
<p className="text-xs text-gray-600 font-medium mb-1">Alt Kategori</p>
<p className="text-lg font-bold text-gray-900">
<div className="bg-blue-50 rounded-xl p-3 border border-blue-100">
<p className="text-xs text-slate-500 font-medium mb-1">Alt Kategori</p>
<p className="text-lg font-bold text-slate-900">
{report.total_subcategories}
</p>
</div>
<div className="bg-green-50 rounded-lg p-3 border border-green-100">
<p className="text-xs text-gray-600 font-medium mb-1">Ürün</p>
<p className="text-lg font-bold text-gray-900">
<div className="bg-green-50 rounded-xl p-3 border border-green-100">
<p className="text-xs text-slate-500 font-medium mb-1">Ürün</p>
<p className="text-lg font-bold text-slate-900">
{report.total_products.toLocaleString()}
</p>
</div>
</div>
{/* Enrichment Progress */}
{isEnriching && (
<div className="mb-4">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-amber-600 font-medium">Zenginleştiriliyor...</span>
<span className="text-xs text-amber-600 font-medium tabular-nums">{enrichProgress}%</span>
</div>
<div className="w-full bg-amber-100 rounded-full h-1.5">
<div
className="bg-amber-500 h-1.5 rounded-full transition-all duration-500"
style={{ width: `${enrichProgress}%` }}
/>
</div>
</div>
)}
{/* Date */}
<div className="text-xs text-gray-500 mb-4">
<div className="text-xs text-slate-400 mb-4">
{formatDate(report.created_at)}
</div>
{/* Actions */}
<div className="flex space-x-2">
<div className="flex flex-wrap gap-2">
<Link
to={`/reports/${report.id}`}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors text-sm text-center"
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-orange-500 text-white rounded-xl font-medium hover:bg-orange-600 transition-colors text-sm text-center"
>
<Eye size={16} />
Görüntüle
</Link>
{!isEnriched && !isEnriching && (
<button
onClick={() => handleEnrich(report.id)}
className="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-50 border border-amber-200 text-amber-700 rounded-xl font-medium hover:bg-amber-100 transition-colors text-sm"
title="Sosyal kanıt verilerini zenginleştir"
>
<Sparkles size={14} />
Zenginleştir
</button>
)}
<button
onClick={() => handleDeleteReport(report.id, report.name)}
className="px-4 py-2 bg-red-500 text-white rounded-lg font-medium hover:bg-red-600 transition-colors text-sm"
className="inline-flex items-center gap-2 px-3 py-2 bg-white border border-red-300 text-red-600 rounded-xl font-medium hover:bg-red-50 transition-colors text-sm"
>
Sil
<Trash2 size={14} />
</button>
</div>
</div>
))}
)
})}
</div>
</div>
)

View File

@@ -1,5 +1,8 @@
import { useState } from 'react'
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ScatterChart, Scatter } from 'recharts'
import KpiCard from '../ui/KpiCard'
import { Barcode, Globe, Flag, Award } from 'lucide-react'
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
export default function BarcodeTab({ barcodeAnalytics }) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: null })
@@ -60,7 +63,7 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (!barcodeAnalytics) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Barkod analizi yükleniyor...</p>
<p className="text-slate-400">Barkod analizi yükleniyor...</p>
</div>
)
}
@@ -70,100 +73,58 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Row 1: KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Total Products with Barcode */}
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm font-medium">Barkodlu Ürün</p>
<p className="text-3xl font-bold mt-2">
{barcodeAnalytics.kpis.totalWithBarcode.toLocaleString('tr-TR')}
</p>
</div>
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
</div>
</div>
<KpiCard
title="Barkodlu Ürün"
value={barcodeAnalytics.kpis.totalWithBarcode.toLocaleString('tr-TR')}
icon={Barcode}
color="blue"
/>
{/* Total Countries */}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm font-medium">Tespit Edilen Ülke</p>
<p className="text-3xl font-bold mt-2">
{barcodeAnalytics.kpis.totalCountries}
</p>
{barcodeAnalytics.kpis.undetectedProducts > 0 && (
<p className="text-green-100 text-xs mt-1">
{barcodeAnalytics.kpis.undetectedProducts.toLocaleString('tr-TR')} tespit edilemedi
</p>
)}
</div>
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
<KpiCard
title="Tespit Edilen Ülke"
value={barcodeAnalytics.kpis.totalCountries}
subtitle={barcodeAnalytics.kpis.undetectedProducts > 0 ? `${barcodeAnalytics.kpis.undetectedProducts.toLocaleString('tr-TR')} tespit edilemedi` : undefined}
icon={Globe}
color="emerald"
/>
{/* Domestic Share */}
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm font-medium">Yerli Ürün Payı</p>
<p className="text-3xl font-bold mt-2">
{barcodeAnalytics.kpis.domesticShare}%
</p>
<p className="text-purple-100 text-xs mt-1">Türkiye menşeili</p>
</div>
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
</div>
</div>
</div>
<KpiCard
title="Yerli Ürün Payı"
value={`${barcodeAnalytics.kpis.domesticShare}%`}
subtitle="Türkiye menşeili"
icon={Flag}
color="violet"
/>
{/* Top Country */}
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm font-medium">En Çok Ürün</p>
<p className="text-2xl font-bold mt-2">
{barcodeAnalytics.kpis.topCountry}
</p>
<p className="text-orange-100 text-xs mt-1">
{barcodeAnalytics.kpis.topCountryShare.toFixed(1)}% pay
</p>
</div>
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
</div>
</div>
<KpiCard
title="En Çok Ürün"
value={barcodeAnalytics.kpis.topCountry}
subtitle={`${barcodeAnalytics.kpis.topCountryShare.toFixed(1)}% pay`}
icon={Award}
color="orange"
/>
</div>
{/* Row 2: Category-Country Heatmap - FULL WIDTH */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kategori-Ülke Isı Haritası (Top 10x10)</h3>
<p className="text-xs text-gray-500 mb-4">Hangi ülkelerin hangi kategorilerde güçlü olduğunu gösterir. Koyu renkler daha yüksek satış hacmini temsil eder.</p>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Kategori-Ülke Isı Haritası (Top 10x10)</h3>
<p className="text-xs text-slate-400 mb-4">Hangi ülkelerin hangi kategorilerde güçlü olduğunu gösterir. Koyu renkler daha yüksek satış hacmini temsil eder.</p>
{barcodeAnalytics.categoryCountryMatrix && barcodeAnalytics.categoryCountryMatrix.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full border-collapse">
<thead>
<tr>
<th className="border border-gray-200 px-2 py-2 bg-gray-50 text-xs font-medium text-gray-700 sticky left-0 z-10">
<th className="border border-slate-200 px-2 py-2 bg-slate-50 text-xs font-medium text-slate-700 sticky left-0 z-10">
Kategori / Ülke
</th>
{barcodeAnalytics.topCountriesForHeatmap.map((country) => (
<th
key={country.name}
className="border border-gray-200 px-2 py-2 bg-gray-50 text-xs font-medium text-gray-700 whitespace-nowrap"
className="border border-slate-200 px-2 py-2 bg-slate-50 text-xs font-medium text-slate-700 whitespace-nowrap"
style={{ minWidth: '80px' }}
>
{country.name}
@@ -174,7 +135,7 @@ export default function BarcodeTab({ barcodeAnalytics }) {
<tbody>
{barcodeAnalytics.topCategories.map((category) => (
<tr key={category}>
<td className="border border-gray-200 px-2 py-2 text-xs font-medium text-gray-900 bg-gray-50 sticky left-0 z-10 whitespace-nowrap">
<td className="border border-slate-200 px-2 py-2 text-xs font-medium text-slate-900 bg-slate-50 sticky left-0 z-10 whitespace-nowrap">
{category}
</td>
{barcodeAnalytics.topCountriesForHeatmap.map((country) => {
@@ -184,8 +145,8 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (!cell) {
return (
<td key={country.name} className="border border-gray-200 px-2 py-2 bg-gray-50">
<div className="text-center text-xs text-gray-400">-</div>
<td key={country.name} className="border border-slate-200 px-2 py-2 bg-slate-50">
<div className="text-center text-xs text-slate-400">-</div>
</td>
)
}
@@ -197,12 +158,12 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (intensity > 0.7) return 'bg-blue-600 text-white'
if (intensity > 0.5) return 'bg-blue-500 text-white'
if (intensity > 0.3) return 'bg-blue-400 text-white'
if (intensity > 0.15) return 'bg-blue-300 text-gray-900'
return 'bg-blue-100 text-gray-900'
if (intensity > 0.15) return 'bg-blue-300 text-slate-900'
return 'bg-blue-100 text-slate-900'
}
return (
<td key={country.name} className="border border-gray-200 p-0">
<td key={country.name} className="border border-slate-200 p-0">
<div
className={`px-2 py-2 ${getColor(intensity)} text-center cursor-help transition-all hover:scale-105`}
title={`${category} - ${country.name}\nSatış: ${cell.orders.toLocaleString('tr-TR')}\nCiro: ₺${cell.revenue.toLocaleString('tr-TR')}\nÜrün: ${cell.productCount}\nOrt: ${cell.avgOrdersPerProduct} satış/ürün`}
@@ -221,45 +182,45 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Legend */}
<div className="mt-4 flex items-center justify-end gap-2 text-xs">
<span className="text-gray-600">Düşük</span>
<span className="text-slate-500">Düşük</span>
<div className="flex gap-1">
<div className="w-6 h-4 bg-blue-100 border border-gray-300"></div>
<div className="w-6 h-4 bg-blue-300 border border-gray-300"></div>
<div className="w-6 h-4 bg-blue-400 border border-gray-300"></div>
<div className="w-6 h-4 bg-blue-500 border border-gray-300"></div>
<div className="w-6 h-4 bg-blue-600 border border-gray-300"></div>
<div className="w-6 h-4 bg-blue-100 border border-slate-300"></div>
<div className="w-6 h-4 bg-blue-300 border border-slate-300"></div>
<div className="w-6 h-4 bg-blue-400 border border-slate-300"></div>
<div className="w-6 h-4 bg-blue-500 border border-slate-300"></div>
<div className="w-6 h-4 bg-blue-600 border border-slate-300"></div>
</div>
<span className="text-gray-600">Yüksek</span>
<span className="text-slate-500">Yüksek</span>
</div>
</div>
) : (
<div className="flex items-center justify-center h-64 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<p className="text-gray-500 text-sm">Veri yükleniyor...</p>
<div className="flex items-center justify-center h-64 bg-slate-50 rounded-xl border-2 border-dashed border-slate-300">
<p className="text-slate-400 text-sm">Veri yükleniyor...</p>
</div>
)}
</div>
{/* Row 3: Top 20 Countries - FULL WIDTH with 10x10 Grid */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-800 mb-4">
En Çok Satan Ülkeler (Top 20)
</h3>
<div className="grid grid-cols-2 gap-4">
{/* Left Column: Countries 1-10 */}
<div className="space-y-2">
{barcodeAnalytics.topByOrders.slice(0, 10).map((country, index) => (
<div key={country.name} className="flex items-center justify-between p-3 bg-gradient-to-r from-blue-50 to-transparent rounded-lg hover:from-blue-100 transition-colors">
<div key={country.name} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-orange-50/30 transition-colors">
<div className="flex items-center gap-3 flex-1">
<span className="flex items-center justify-center w-8 h-8 bg-blue-500 text-white rounded-full font-bold text-sm">
<span className="flex items-center justify-center w-8 h-8 bg-orange-500 text-white rounded-full font-bold text-sm">
{index + 1}
</span>
<span className="font-semibold text-gray-800">{country.name}</span>
<span className="font-semibold text-slate-800">{country.name}</span>
</div>
<div className="text-right space-y-1">
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Satış:</span> {country.totalOrders.toLocaleString('tr-TR')}
</p>
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Ciro:</span> {country.totalRevenue.toLocaleString('tr-TR')}
</p>
</div>
@@ -270,18 +231,18 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Right Column: Countries 11-20 */}
<div className="space-y-2">
{barcodeAnalytics.topByOrders.slice(10, 20).map((country, index) => (
<div key={country.name} className="flex items-center justify-between p-3 bg-gradient-to-r from-purple-50 to-transparent rounded-lg hover:from-purple-100 transition-colors">
<div key={country.name} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-orange-50/30 transition-colors">
<div className="flex items-center gap-3 flex-1">
<span className="flex items-center justify-center w-8 h-8 bg-purple-500 text-white rounded-full font-bold text-sm">
{index + 11}
</span>
<span className="font-semibold text-gray-800">{country.name}</span>
<span className="font-semibold text-slate-800">{country.name}</span>
</div>
<div className="text-right space-y-1">
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Satış:</span> {country.totalOrders.toLocaleString('tr-TR')}
</p>
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Ciro:</span> {country.totalRevenue.toLocaleString('tr-TR')}
</p>
</div>
@@ -294,8 +255,8 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Row 4: Charts - 50/50 Split */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Country Sales Bar Chart (Top 15) */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ülke Bazlı Satış (Top 15)</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Ülke Bazlı Satış (Top 15)</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={barcodeAnalytics.topByOrders.slice(0, 15)}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
@@ -309,16 +270,16 @@ export default function BarcodeTab({ barcodeAnalytics }) {
<YAxis tick={{ fontSize: 12 }} />
<Tooltip
formatter={(value) => value.toLocaleString('tr-TR')}
contentStyle={{ fontSize: '12px' }}
{...CHART_TOOLTIP_STYLE}
/>
<Bar dataKey="totalOrders" fill="#3b82f6" name="Satış Adedi" />
<Bar dataKey="totalOrders" fill={CHART_COLORS[0]} name="Satış Adedi" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Price vs Revenue Scatter Chart */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ortalama Fiyat / Ciro İlişkisi</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Ortalama Fiyat / Ciro İlişkisi</h3>
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
@@ -341,12 +302,12 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3">
<p className="font-semibold text-gray-900 mb-2 text-sm">{data.name}</p>
<p className="font-semibold text-gray-900 text-sm">
<div className="bg-slate-800 border-none rounded-xl shadow-lg p-3">
<p className="font-semibold text-slate-50 mb-2 text-sm">{data.name}</p>
<p className="font-semibold text-slate-200 text-sm">
Ciro: {data.totalRevenue.toLocaleString('tr-TR')}
</p>
<p className="font-semibold text-gray-900 text-sm">
<p className="font-semibold text-slate-200 text-sm">
Satış: {data.totalOrders.toLocaleString('tr-TR')}
</p>
</div>
@@ -357,91 +318,91 @@ export default function BarcodeTab({ barcodeAnalytics }) {
/>
<Scatter
data={barcodeAnalytics.countries.filter(c => c.totalRevenue > 0)}
fill="#8b5cf6"
fill={CHART_COLORS[3]}
name="Ülkeler"
/>
</ScatterChart>
</ResponsiveContainer>
<p className="text-xs text-gray-500 mt-2 text-center">
<p className="text-xs text-slate-400 mt-2 text-center">
Her nokta bir ülkeyi temsil eder (Ortalama fiyat vs Toplam ciro)
</p>
</div>
</div>
{/* Row 5: Detailed Country Comparison Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Ülke Karşılaştırması</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Ülke Karşılaştırması</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b-2 border-gray-200 bg-gray-50">
<thead className="border-b-2 border-slate-200 bg-slate-50">
<tr>
<th
className="px-4 py-3 text-left text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-left text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('name')}
>
Ülke{renderSortIndicator('name')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('productCount')}
>
Ürün{renderSortIndicator('productCount')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('totalOrders')}
>
Satış{renderSortIndicator('totalOrders')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('totalRevenue')}
>
Ciro{renderSortIndicator('totalRevenue')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('marketShare')}
>
Pay %{renderSortIndicator('marketShare')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('avgPrice')}
>
Ort. Fiyat{renderSortIndicator('avgPrice')}
</th>
<th
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('categoryCount')}
>
Kategori{renderSortIndicator('categoryCount')}
</th>
<th
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('brandCount')}
>
Marka{renderSortIndicator('brandCount')}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tbody className="divide-y divide-slate-100">
{getSortedData().map((country) => (
<tr key={country.name} className="hover:bg-gray-50">
<tr key={country.name} className="hover:bg-orange-50/30">
<td className="px-4 py-3">
<div className="text-sm font-semibold text-gray-900">{country.name}</div>
<div className="text-sm font-semibold text-slate-900">{country.name}</div>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">{country.productCount}</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">{country.totalOrders.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">{country.totalRevenue.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm text-slate-900">{country.productCount}</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">{country.totalOrders.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">{country.totalRevenue.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-600">
{country.marketShare.toFixed(1)}%
</span>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">{country.avgPrice.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-center text-sm text-gray-900">{country.categoryCount}</td>
<td className="px-4 py-3 text-center text-sm text-gray-900">{country.brandCount}</td>
<td className="px-4 py-3 text-right text-sm text-slate-900">{country.avgPrice.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-center text-sm text-slate-900">{country.categoryCount}</td>
<td className="px-4 py-3 text-center text-sm text-slate-900">{country.brandCount}</td>
</tr>
))}
</tbody>

View File

@@ -1,5 +1,8 @@
import { useState, useMemo, useEffect } from 'react'
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, ScatterChart, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, BarChart, Bar } from 'recharts'
import KpiCard from '../ui/KpiCard'
import { Tag, Trophy, Package, PieChart as PieChartIcon } from 'lucide-react'
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort, brandSortConfig }) {
// Pagination state
@@ -23,7 +26,7 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
if (!brandAnalytics) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Brand analizi yükleniyor...</p>
<p className="text-slate-400">Brand analizi yükleniyor...</p>
</div>
)
}
@@ -32,113 +35,73 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
<div className="space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm font-medium">Toplam Marka</p>
<p className="text-3xl font-bold mt-2">
{brandAnalytics.kpis.totalBrands.toLocaleString('tr-TR')}
</p>
<p className="text-blue-100 text-xs mt-1">Benzersiz marka sayısı</p>
</div>
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm font-medium">Lider Marka Payı</p>
<p className="text-3xl font-bold mt-2">
%{brandAnalytics.kpis.leaderShare}
</p>
<p className="text-green-100 text-xs mt-1">{brandAnalytics.topByOrders[0]?.name}</p>
</div>
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm font-medium">Ort. Marka Başına Ürün</p>
<p className="text-3xl font-bold mt-2">
{brandAnalytics.kpis.avgProductsPerBrand}
</p>
<p className="text-purple-100 text-xs mt-1">Ürün çeşitliliği</p>
</div>
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm font-medium">Pazar Yoğunlaşması</p>
<p className="text-3xl font-bold mt-2">
{brandAnalytics.kpis.hhi}
</p>
<p className="text-orange-100 text-xs mt-1">{brandAnalytics.kpis.marketConcentration} (HHI)</p>
</div>
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
</svg>
</div>
</div>
</div>
<KpiCard
title="Toplam Marka"
value={brandAnalytics.kpis.totalBrands.toLocaleString('tr-TR')}
subtitle="Benzersiz marka sayısı"
icon={Tag}
color="blue"
/>
<KpiCard
title="Lider Marka Payı"
value={`%${brandAnalytics.kpis.leaderShare}`}
subtitle={brandAnalytics.topByOrders[0]?.name}
icon={Trophy}
color="emerald"
/>
<KpiCard
title="Ort. Marka Başına Ürün"
value={brandAnalytics.kpis.avgProductsPerBrand}
subtitle="Ürün çeşitliliği"
icon={Package}
color="violet"
/>
<KpiCard
title="Pazar Yoğunlaşması"
value={brandAnalytics.kpis.hhi}
subtitle={`${brandAnalytics.kpis.marketConcentration} (HHI)`}
icon={PieChartIcon}
color="orange"
/>
</div>
{/* Row 2: Top 20 Brands Full-Width Two-Column Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satan Markalar (Top 20)</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satan Markalar (Top 20)</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column - Brands 1-10 */}
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">#</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Marka</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Satış</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Ciro</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Pay %</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">#</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">Marka</th>
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Satış</th>
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Ciro</th>
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Pay %</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tbody className="divide-y divide-slate-200">
{brandAnalytics.topByOrders.slice(0, 10).map((brand, index) => {
const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1)
return (
<tr key={brand.name} className="hover:bg-gray-50">
<td className="px-3 py-2 text-sm font-medium text-gray-900">{index + 1}</td>
<tr key={brand.name} className="hover:bg-orange-50/30 even:bg-slate-50/50">
<td className="px-3 py-2 text-sm font-medium text-slate-900">{index + 1}</td>
<td className="px-3 py-2">
<a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
>
{brand.name}
</a>
<p className="text-xs text-gray-500">{brand.productCount} ürün</p>
<p className="text-xs text-slate-400">{brand.productCount} ürün</p>
</td>
<td className="px-3 py-2 text-right text-sm text-gray-900">
<td className="px-3 py-2 text-right text-sm text-slate-900">
{brand.totalOrders.toLocaleString('tr-TR')}
</td>
<td className="px-3 py-2 text-right text-sm text-gray-900">
<td className="px-3 py-2 text-right text-sm text-slate-900">
{Math.round(brand.totalRevenue).toLocaleString('tr-TR')}
</td>
<td className="px-3 py-2 text-right text-sm font-semibold text-green-600">
@@ -154,36 +117,36 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
{/* Right Column - Brands 11-20 */}
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50">
<thead className="bg-slate-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">#</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Marka</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Satış</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Ciro</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Pay %</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">#</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">Marka</th>
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Satış</th>
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Ciro</th>
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Pay %</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tbody className="divide-y divide-slate-200">
{brandAnalytics.topByOrders.slice(10, 20).map((brand, index) => {
const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1)
return (
<tr key={brand.name} className="hover:bg-gray-50">
<td className="px-3 py-2 text-sm font-medium text-gray-900">{index + 11}</td>
<tr key={brand.name} className="hover:bg-orange-50/30 even:bg-slate-50/50">
<td className="px-3 py-2 text-sm font-medium text-slate-900">{index + 11}</td>
<td className="px-3 py-2">
<a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
>
{brand.name}
</a>
<p className="text-xs text-gray-500">{brand.productCount} ürün</p>
<p className="text-xs text-slate-400">{brand.productCount} ürün</p>
</td>
<td className="px-3 py-2 text-right text-sm text-gray-900">
<td className="px-3 py-2 text-right text-sm text-slate-900">
{brand.totalOrders.toLocaleString('tr-TR')}
</td>
<td className="px-3 py-2 text-right text-sm text-gray-900">
<td className="px-3 py-2 text-right text-sm text-slate-900">
{Math.round(brand.totalRevenue).toLocaleString('tr-TR')}
</td>
<td className="px-3 py-2 text-right text-sm font-semibold text-green-600">
@@ -201,8 +164,8 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
{/* Row 3: Market Share Chart & Price/Performance Matrix */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Market Share Pie Chart */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Pazar Payı Dağılımı</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Pazar Payı Dağılımı</h3>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
@@ -221,23 +184,23 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
fill="#8884d8"
dataKey="value"
>
{['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#6b7280'].map((color, index) => (
{CHART_COLORS.slice(0, 6).map((color, index) => (
<Cell key={`cell-${index}`} fill={color} />
))}
</Pie>
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} />
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} {...CHART_TOOLTIP_STYLE} />
</PieChart>
</ResponsiveContainer>
<div className="mt-4 p-3 bg-blue-50 rounded">
<p className="text-sm text-gray-700">
Top 3 marka pazarın <span className="font-bold text-blue-600">%{brandAnalytics.kpis.top3Share}</span>'ini kontrol ediyor
<div className="mt-4 p-3 bg-slate-50 rounded-lg">
<p className="text-sm text-slate-700">
Top 3 marka pazarın <span className="font-bold text-orange-500">%{brandAnalytics.kpis.top3Share}</span>'ini kontrol ediyor
</p>
</div>
</div>
{/* Price/Quality Scatter Plot */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat/Performans Matrisi</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat/Performans Matrisi</h3>
<ResponsiveContainer width="100%" height={300}>
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" />
@@ -260,7 +223,7 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div className="bg-white p-3 border border-gray-200 rounded shadow-lg">
<div className="bg-white p-3 border border-slate-200 rounded-lg shadow-lg">
<p className="font-semibold">{data.name}</p>
<p className="text-sm">Fiyat: ₺{data.avgPrice.toLocaleString('tr-TR')}</p>
<p className="text-sm">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
@@ -273,32 +236,32 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
/>
<Scatter
data={brandAnalytics.topByOrders.slice(0, 15)}
fill="#3b82f6"
fill={CHART_COLORS[0]}
opacity={0.6}
/>
</ScatterChart>
</ResponsiveContainer>
<p className="text-xs text-gray-500 mt-2 text-center">
<p className="text-xs text-slate-400 mt-2 text-center">
Kabarcık boyutu: Sosyal kanıt skoru (görüntülenme + satış + ürün çeşitliliği)
</p>
</div>
</div>
{/* Row 4: Detailed Brand Comparison Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Marka Karşılaştırması</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Marka Karşılaştırması</h3>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => handleBrandSort('name')}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-left text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
>
<div className="flex items-center gap-1">
Marka
{brandSortConfig.key === 'name' && (
<span className="text-gray-400">
<span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''}
</span>
)}
@@ -306,12 +269,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th>
<th
onClick={() => handleBrandSort('totalOrders')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
>
<div className="flex items-center justify-end gap-1">
Satış
{brandSortConfig.key === 'totalOrders' && (
<span className="text-gray-400">
<span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''}
</span>
)}
@@ -319,12 +282,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th>
<th
onClick={() => handleBrandSort('totalRevenue')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
>
<div className="flex items-center justify-end gap-1">
Ciro
{brandSortConfig.key === 'totalRevenue' && (
<span className="text-gray-400">
<span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''}
</span>
)}
@@ -332,12 +295,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th>
<th
onClick={() => handleBrandSort('productCount')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
>
<div className="flex items-center justify-end gap-1">
Ürün Sayısı
{brandSortConfig.key === 'productCount' && (
<span className="text-gray-400">
<span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''}
</span>
)}
@@ -345,12 +308,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th>
<th
onClick={() => handleBrandSort('categoryCount')}
className="px-4 py-3 text-center text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-center text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
>
<div className="flex items-center justify-center gap-1">
Kategori Çeşitliliği
{brandSortConfig.key === 'categoryCount' && (
<span className="text-gray-400">
<span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''}
</span>
)}
@@ -358,41 +321,41 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th>
<th
onClick={() => handleBrandSort('avgPrice')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
>
<div className="flex items-center justify-end gap-1">
Ort. Fiyat
{brandSortConfig.key === 'avgPrice' && (
<span className="text-gray-400">
<span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''}
</span>
)}
</div>
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500">Fiyat Aralığı</th>
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Segment</th>
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">Fiyat Aralığı</th>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-400">Segment</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tbody className="divide-y divide-slate-200">
{paginatedBrands.map(brand => (
<tr key={brand.name} className="hover:bg-gray-50">
<tr key={brand.name} className="hover:bg-orange-50/30 even:bg-slate-50/50">
<td className="px-4 py-3">
<a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
>
{brand.name}
</a>
</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">
{brand.totalOrders.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600">
₺{brand.totalRevenue.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">
<td className="px-4 py-3 text-right text-sm text-slate-900">
{brand.productCount}
</td>
<td className="px-4 py-3 text-center">
@@ -401,10 +364,10 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
<span className="text-xs text-indigo-600">kategori</span>
</div>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">
<td className="px-4 py-3 text-right text-sm text-slate-900">
₺{brand.avgPrice.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right text-xs text-gray-600">
<td className="px-4 py-3 text-right text-xs text-slate-500">
{brand.minPrice !== brand.maxPrice
? `₺${brand.minPrice.toLocaleString('tr-TR')} - ₺${brand.maxPrice.toLocaleString('tr-TR')}`
: `₺${brand.minPrice.toLocaleString('tr-TR')}`
@@ -429,23 +392,23 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="mt-4 pt-4 border-t border-slate-200">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
<div className="text-sm text-slate-500">
Sayfa {currentPage} / {totalPages} (Toplam {sortedBrands.length.toLocaleString('tr-TR')} marka)
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
««
</button>
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Önceki
</button>
@@ -470,8 +433,8 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
onClick={() => setCurrentPage(pageNum)}
className={`px-3 py-1.5 text-sm border rounded-xl transition-colors ${
currentPage === pageNum
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 hover:bg-gray-50'
? 'bg-orange-500 text-white border-orange-500'
: 'border-slate-300 hover:bg-orange-50/30'
}`}
>
{pageNum}
@@ -483,14 +446,14 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Sonraki
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
»»
</button>
@@ -501,19 +464,19 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</div>
{/* Row 5: Market Insights */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
<div className="bg-slate-50 border border-slate-200 rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">
Pazar İçgörüleri
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Lider Marka Dominansı</p>
<p className="text-lg font-bold text-blue-600 mt-1">
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Lider Marka Dominansı</p>
<p className="text-lg font-bold text-orange-500 mt-1">
{brandAnalytics.topByOrders[0]?.name} pazarın %{brandAnalytics.kpis.leaderShare}'ini kontrol ediyor
</p>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Segment Analizi</p>
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Segment Analizi</p>
<p className="text-lg font-bold text-purple-600 mt-1">
{Object.entries(brandAnalytics.priceSegments)
.sort((a, b) => b[1].length - a[1].length)[0][0]}
@@ -521,14 +484,14 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
.sort((a, b) => b[1].length - a[1].length)[0][1].length} marka)
</p>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Pazar Yapısı</p>
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Pazar Yapısı</p>
<p className="text-lg font-bold text-green-600 mt-1">
{brandAnalytics.kpis.marketConcentration} - HHI: {brandAnalytics.kpis.hhi}
</p>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Rekabet Yoğunluğu</p>
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Rekabet Yoğunluğu</p>
<p className="text-lg font-bold text-orange-600 mt-1">
Top 3 marka toplam satışın %{brandAnalytics.kpis.top3Share}'i
</p>

View File

@@ -1,10 +1,13 @@
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, PieChart, Pie, Cell, Legend, ScatterChart, Scatter, ZAxis } from 'recharts'
import KpiCard from '../ui/KpiCard'
import { Grid3X3, Trophy, BarChart3, PieChart as PieChartIcon } from 'lucide-react'
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
export default function CategoryTab({ categoryAnalytics, sortedCategories, handleCategorySort, categorySortConfig }) {
if (!categoryAnalytics) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Category analizi yükleniyor...</p>
<p className="text-slate-400">Category analizi yükleniyor...</p>
</div>
)
}
@@ -13,95 +16,66 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
<div className="space-y-6">
{/* Row 1: KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm font-medium">Toplam Kategori</p>
<p className="text-3xl font-bold mt-2">{categoryAnalytics.kpis.totalCategories}</p>
</div>
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm font-medium">Lider Kategori Payı</p>
<p className="text-3xl font-bold mt-2">%{categoryAnalytics.kpis.leaderShare}</p>
</div>
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm font-medium">Ort. Ürün/Kategori</p>
<p className="text-3xl font-bold mt-2">{categoryAnalytics.kpis.avgProductsPerCategory}</p>
</div>
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
</svg>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm font-medium">Pazar Yoğunlaşması</p>
<p className="text-2xl font-bold mt-2">{categoryAnalytics.kpis.marketConcentration}</p>
<p className="text-orange-100 text-xs mt-1">HHI: {categoryAnalytics.kpis.hhi}</p>
</div>
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
</svg>
</div>
</div>
</div>
<KpiCard
title="Toplam Kategori"
value={categoryAnalytics.kpis.totalCategories}
icon={Grid3X3}
color="blue"
/>
<KpiCard
title="Lider Kategori Payı"
value={`%${categoryAnalytics.kpis.leaderShare}`}
icon={Trophy}
color="violet"
/>
<KpiCard
title="Ort. Ürün/Kategori"
value={categoryAnalytics.kpis.avgProductsPerCategory}
icon={BarChart3}
color="emerald"
/>
<KpiCard
title="Pazar Yoğunlaşması"
value={categoryAnalytics.kpis.marketConcentration}
subtitle={`HHI: ${categoryAnalytics.kpis.hhi}`}
icon={PieChartIcon}
color="orange"
/>
</div>
{/* Row 2: Top Categories Table + Price Positioning */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* En Çok Satan Kategoriler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satan Kategoriler (Top 20)</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satan Kategoriler (Top 20)</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-gray-200">
<thead className="border-b border-slate-200">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">#</th>
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">Kategori</th>
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Satış</th>
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Ciro</th>
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Pay %</th>
<th className="px-4 py-3 text-left text-sm font-medium text-slate-400">#</th>
<th className="px-4 py-3 text-left text-sm font-medium text-slate-400">Kategori</th>
<th className="px-4 py-3 text-right text-sm font-medium text-slate-400">Satış</th>
<th className="px-4 py-3 text-right text-sm font-medium text-slate-400">Ciro</th>
<th className="px-4 py-3 text-right text-sm font-medium text-slate-400">Pay %</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tbody className="divide-y divide-slate-100">
{categoryAnalytics.topByOrders.slice(0, 20).map((category, index) => (
<tr key={category.name} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm text-gray-900 font-medium">{index + 1}</td>
<tr key={category.name} className="hover:bg-orange-50/30">
<td className="px-4 py-3 text-sm text-slate-900 font-medium">{index + 1}</td>
<td className="px-4 py-3">
<a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700 font-medium"
className="text-orange-500 hover:text-orange-600 font-medium"
>
{category.name}
</a>
<div className="text-xs text-gray-600 mt-1">{category.productCount} ürün</div>
<div className="text-xs text-slate-500 mt-1">{category.productCount} ürün</div>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">{category.totalOrders.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">{category.totalRevenue.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm text-slate-900">{category.totalOrders.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm text-slate-900">{category.totalRevenue.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600">{category.marketShare.toFixed(1)}%</td>
</tr>
))}
@@ -111,11 +85,11 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</div>
{/* Fiyat Pozisyonlaması */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat Pozisyonlaması</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat Pozisyonlaması</h3>
<div className="space-y-4">
{Object.entries(categoryAnalytics.priceSegments).map(([segment, categories]) => (
<div key={segment} className="bg-gray-50 rounded-lg p-4">
<div key={segment} className="bg-slate-50 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className={`px-3 py-1 text-xs font-medium rounded-full ${
@@ -127,15 +101,15 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
}`}>
{segment}
</span>
<span className="text-sm font-semibold text-gray-700">
<span className="text-sm font-semibold text-slate-700">
{categories.length} kategori
</span>
</div>
<span className="text-sm text-gray-600">
<span className="text-sm text-slate-500">
%{Math.round((categories.length / categoryAnalytics.categories.length) * 100)}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="w-full bg-slate-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
segment === 'Premium'
@@ -147,7 +121,7 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
style={{ width: `${(categories.length / categoryAnalytics.categories.length) * 100}%` }}
></div>
</div>
<p className="text-xs text-gray-500 mt-2">
<p className="text-xs text-slate-400 mt-2">
Toplam satış: {categories.reduce((sum, c) => sum + c.totalOrders, 0).toLocaleString('tr-TR')}
</p>
</div>
@@ -159,8 +133,8 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
{/* Row 3: Market Share Chart + Price/Performance Matrix */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Pazar Payı Dağılımı */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kategori Pazar Payı Dağılımı</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Kategori Pazar Payı Dağılımı</h3>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
@@ -185,21 +159,21 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
dataKey="value"
>
{[0, 1, 2, 3, 4, 5].map((index) => (
<Cell key={`cell-${index}`} fill={['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#6b7280'][index]} />
<Cell key={`cell-${index}`} fill={CHART_COLORS[index]} />
))}
</Pie>
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} />
<Tooltip {...CHART_TOOLTIP_STYLE} formatter={(value) => value.toLocaleString('tr-TR')} />
<Legend />
</PieChart>
</ResponsiveContainer>
<p className="text-xs text-gray-500 mt-2 text-center">
<p className="text-xs text-slate-400 mt-2 text-center">
Pazar payı satış adedine göre hesaplanmıştır
</p>
</div>
{/* Fiyat/Performans Matrisi */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat/Performans Matrisi</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat/Performans Matrisi</h3>
<ResponsiveContainer width="100%" height={300}>
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid />
@@ -212,11 +186,11 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div className="bg-white p-3 border border-gray-300 rounded shadow-lg">
<p className="font-semibold text-gray-900">{data.name}</p>
<p className="text-sm text-gray-600">Ort. Fiyat: {data.avgPrice.toLocaleString('tr-TR')}</p>
<p className="text-sm text-gray-600">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
<p className="text-sm text-gray-600">Sosyal Skor: {data.socialScore}</p>
<div className="bg-slate-800 p-3 border-none rounded-xl shadow-lg">
<p className="font-semibold text-slate-50">{data.name}</p>
<p className="text-sm text-slate-200">Ort. Fiyat: {data.avgPrice.toLocaleString('tr-TR')}</p>
<p className="text-sm text-slate-200">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
<p className="text-sm text-slate-200">Sosyal Skor: {data.socialScore}</p>
</div>
)
}
@@ -227,27 +201,27 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
<Scatter name="Kategoriler" data={categoryAnalytics.topByOrders.slice(0, 15)} fill="#8b5cf6" />
</ScatterChart>
</ResponsiveContainer>
<p className="text-xs text-gray-500 mt-2 text-center">
<p className="text-xs text-slate-400 mt-2 text-center">
Kabarcık boyutu: Sosyal kanıt skoru (görüntülenme + satış + ürün çeşitliliği)
</p>
</div>
</div>
{/* Row 4: Detailed Category Comparison Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Kategori Karşılaştırması</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Kategori Karşılaştırması</h3>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50">
<thead className="bg-slate-50">
<tr>
<th
onClick={() => handleCategorySort('name')}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-left text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
>
<div className="flex items-center gap-1">
Kategori
{categorySortConfig.key === 'name' && (
<span className="text-gray-400">
<span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span>
)}
@@ -255,12 +229,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th>
<th
onClick={() => handleCategorySort('totalOrders')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
>
<div className="flex items-center justify-end gap-1">
Satış
{categorySortConfig.key === 'totalOrders' && (
<span className="text-gray-400">
<span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span>
)}
@@ -268,12 +242,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th>
<th
onClick={() => handleCategorySort('totalRevenue')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
>
<div className="flex items-center justify-end gap-1">
Ciro
{categorySortConfig.key === 'totalRevenue' && (
<span className="text-gray-400">
<span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span>
)}
@@ -281,12 +255,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th>
<th
onClick={() => handleCategorySort('brandCount')}
className="px-4 py-3 text-center text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-center text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
>
<div className="flex items-center justify-center gap-1">
Marka Çeşitliliği
{categorySortConfig.key === 'brandCount' && (
<span className="text-gray-400">
<span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span>
)}
@@ -294,35 +268,35 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th>
<th
onClick={() => handleCategorySort('avgPrice')}
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
>
<div className="flex items-center justify-end gap-1">
Ort. Fiyat
{categorySortConfig.key === 'avgPrice' && (
<span className="text-gray-400">
<span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span>
)}
</div>
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500">Fiyat Aralığı</th>
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Segment</th>
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">Fiyat Aralığı</th>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-400">Segment</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tbody className="divide-y divide-slate-200">
{sortedCategories.map(category => (
<tr key={category.name} className="hover:bg-gray-50">
<tr key={category.name} className="hover:bg-orange-50/30">
<td className="px-4 py-3">
<a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
>
{category.name}
</a>
</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">
{category.totalOrders.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600">
@@ -334,10 +308,10 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
<span className="text-xs text-indigo-600">marka</span>
</div>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">
<td className="px-4 py-3 text-right text-sm text-slate-900">
{category.avgPrice.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right text-xs text-gray-600">
<td className="px-4 py-3 text-right text-xs text-slate-500">
{category.minPrice !== category.maxPrice
? `${category.minPrice.toLocaleString('tr-TR')} - ₺${category.maxPrice.toLocaleString('tr-TR')}`
: `${category.minPrice.toLocaleString('tr-TR')}`
@@ -362,19 +336,19 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</div>
{/* Row 5: Category Insights */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
<div className="bg-slate-50 border border-slate-200 rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">
Kategori İçgörüleri
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Lider Kategori Dominansı</p>
<p className="text-lg font-bold text-blue-600 mt-1">
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Lider Kategori Dominansı</p>
<p className="text-lg font-bold text-orange-500 mt-1">
{categoryAnalytics.topByOrders[0]?.name} pazarın %{categoryAnalytics.kpis.leaderShare}'ini kontrol ediyor
</p>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Segment Analizi</p>
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Segment Analizi</p>
<p className="text-lg font-bold text-purple-600 mt-1">
{Object.entries(categoryAnalytics.priceSegments)
.sort((a, b) => b[1].length - a[1].length)[0][0]}
@@ -382,14 +356,14 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
.sort((a, b) => b[1].length - a[1].length)[0][1].length} kategori)
</p>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Pazar Yapısı</p>
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Pazar Yapısı</p>
<p className="text-lg font-bold text-green-600 mt-1">
{categoryAnalytics.kpis.marketConcentration} - HHI: {categoryAnalytics.kpis.hhi}
</p>
</div>
<div className="bg-white rounded-lg p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Rekabet Yoğunluğu</p>
<div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-slate-700">Rekabet Yoğunluğu</p>
<p className="text-lg font-bold text-orange-600 mt-1">
Top 3 kategori toplam satışın %{categoryAnalytics.kpis.top3Share}'i
</p>

View File

@@ -0,0 +1,316 @@
import { useState, useMemo } from 'react'
import { Trophy, Star, TrendingUp, Filter, ChevronDown, ChevronUp, ExternalLink } from 'lucide-react'
import KpiCard from '../ui/KpiCard'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../../config/api'
export default function HiddenChampionsTab({ reportId }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [loaded, setLoaded] = useState(false)
// Filters
const [minRating, setMinRating] = useState(4.0)
const [maxReview, setMaxReview] = useState(100)
const [minOrders, setMinOrders] = useState(5)
const [sortKey, setSortKey] = useState('performance_score')
const [sortDir, setSortDir] = useState('desc')
const [showFilters, setShowFilters] = useState(false)
// Fetch data on first render
useState(() => {
if (!loaded && reportId) {
setLoading(true)
fetchWithTimeout(
`${API_URL}/api/reports/${reportId}/hidden-champions`,
{},
TIMEOUT_CONFIG.DASHBOARD
)
.then(res => {
if (!res.ok) throw new Error('Gizli şampiyonlar yüklenemedi')
return res.json()
})
.then(result => {
setData(result)
setLoaded(true)
})
.catch(err => setError(err.message))
.finally(() => setLoading(false))
}
})
// Filtered & sorted products
const filteredProducts = useMemo(() => {
if (!data?.products) return []
return data.products
.filter(p => {
const rating = p.rating || 0
const reviewCount = p.review_count || p.reviewCount || 0
const orders = p.orders || 0
return rating >= minRating && reviewCount <= maxReview && orders >= minOrders
})
.sort((a, b) => {
const aVal = a[sortKey] || 0
const bVal = b[sortKey] || 0
return sortDir === 'desc' ? bVal - aVal : aVal - bVal
})
}, [data, minRating, maxReview, minOrders, sortKey, sortDir])
// KPIs
const kpis = useMemo(() => {
if (!filteredProducts.length) return { count: 0, avgRating: 0, avgPrice: 0 }
const count = filteredProducts.length
const avgRating = (filteredProducts.reduce((s, p) => s + (p.rating || 0), 0) / count).toFixed(1)
const avgPrice = Math.round(filteredProducts.reduce((s, p) => s + (p.price || 0), 0) / count)
return { count, avgRating, avgPrice }
}, [filteredProducts])
const handleSort = (key) => {
if (sortKey === key) {
setSortDir(prev => prev === 'desc' ? 'asc' : 'desc')
} else {
setSortKey(key)
setSortDir('desc')
}
}
const SortIcon = ({ column }) => {
if (sortKey !== column) return <ChevronDown size={14} className="text-slate-300" />
return sortDir === 'desc'
? <ChevronDown size={14} className="text-orange-500" />
: <ChevronUp size={14} className="text-orange-500" />
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-slate-400 flex items-center gap-2">
<div className="w-5 h-5 border-2 border-orange-500 border-t-transparent rounded-full animate-spin" />
Gizli şampiyonlar analiz ediliyor...
</div>
</div>
)
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<p className="text-red-700">{error}</p>
</div>
)
}
if (!data) return null
return (
<div className="space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<KpiCard
title="Gizli Şampiyon"
value={kpis.count}
icon={Trophy}
color="amber"
/>
<KpiCard
title="Ortalama Rating"
value={kpis.avgRating}
icon={Star}
color="emerald"
/>
<KpiCard
title="Ortalama Fiyat"
value={`${kpis.avgPrice.toLocaleString('tr-TR')}`}
icon={TrendingUp}
color="blue"
/>
</div>
{/* Filters */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<button
onClick={() => setShowFilters(!showFilters)}
className="w-full flex items-center justify-between px-6 py-4 hover:bg-slate-50 transition-colors"
>
<div className="flex items-center gap-2">
<Filter size={16} className="text-slate-400" />
<span className="text-sm font-medium text-slate-700">Filtreler</span>
</div>
<ChevronDown size={16} className={`text-slate-400 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
</button>
{showFilters && (
<div className="px-6 pb-4 border-t border-slate-100 pt-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Min Rating</label>
<input
type="number"
step="0.1"
min="0"
max="5"
value={minRating}
onChange={e => setMinRating(parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Max Yorum Sayısı</label>
<input
type="number"
min="0"
value={maxReview}
onChange={e => setMaxReview(parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Min Sipariş</label>
<input
type="number"
min="0"
value={minOrders}
onChange={e => setMinOrders(parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
/>
</div>
</div>
</div>
)}
</div>
{/* Products Table */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h3 className="text-lg font-semibold text-slate-900">Gizli Şampiyonlar</h3>
<p className="text-xs text-slate-400 mt-1">
Yüksek rating, düşük yorum sayısı ancak iyi satış performansı gösteren ürünler
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-100">
<th className="text-left px-4 py-3 font-medium text-slate-500">#</th>
<th className="text-left px-4 py-3 font-medium text-slate-500">Ürün</th>
<th className="text-left px-4 py-3 font-medium text-slate-500">Marka</th>
<th
className="text-right px-4 py-3 font-medium text-slate-500 cursor-pointer hover:text-slate-700"
onClick={() => handleSort('rating')}
>
<div className="flex items-center justify-end gap-1">
Rating <SortIcon column="rating" />
</div>
</th>
<th
className="text-right px-4 py-3 font-medium text-slate-500 cursor-pointer hover:text-slate-700"
onClick={() => handleSort('review_count')}
>
<div className="flex items-center justify-end gap-1">
Yorum <SortIcon column="review_count" />
</div>
</th>
<th
className="text-right px-4 py-3 font-medium text-slate-500 cursor-pointer hover:text-slate-700"
onClick={() => handleSort('price')}
>
<div className="flex items-center justify-end gap-1">
Fiyat <SortIcon column="price" />
</div>
</th>
<th
className="text-right px-4 py-3 font-medium text-slate-500 cursor-pointer hover:text-slate-700"
onClick={() => handleSort('orders')}
>
<div className="flex items-center justify-end gap-1">
Sipariş <SortIcon column="orders" />
</div>
</th>
<th
className="text-right px-4 py-3 font-medium text-slate-500 cursor-pointer hover:text-slate-700"
onClick={() => handleSort('performance_score')}
>
<div className="flex items-center justify-end gap-1">
Skor <SortIcon column="performance_score" />
</div>
</th>
</tr>
</thead>
<tbody>
{filteredProducts.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-12 text-slate-400">
Filtrelere uygun ürün bulunamadı
</td>
</tr>
) : (
filteredProducts.slice(0, 50).map((product, index) => (
<tr key={product.id || index} className="border-b border-slate-50 hover:bg-slate-50/50">
<td className="px-4 py-3 text-slate-400">{index + 1}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2 max-w-xs">
{product.image_url && (
<img src={product.image_url} alt="" className="w-10 h-10 rounded object-cover shrink-0" />
)}
<div className="min-w-0">
<p className="text-sm font-medium text-slate-900 truncate">{product.name}</p>
{product.url && (
<a
href={product.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-orange-500 hover:underline inline-flex items-center gap-0.5"
>
Görüntüle <ExternalLink size={10} />
</a>
)}
</div>
</div>
</td>
<td className="px-4 py-3 text-slate-600">{product.brand || '-'}</td>
<td className="px-4 py-3 text-right">
<span className="inline-flex items-center gap-1 text-amber-600 font-medium">
<Star size={12} className="fill-amber-400 text-amber-400" />
{(product.rating || 0).toFixed(1)}
</span>
</td>
<td className="px-4 py-3 text-right text-slate-600">
{(product.review_count || product.reviewCount || 0).toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right font-medium text-slate-900">
{(product.price || 0).toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-right font-medium text-emerald-600">
{(product.orders || 0).toLocaleString('tr-TR')}
</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
? 'bg-emerald-100 text-emerald-700'
: (product.performance_score || 0) >= 40
? 'bg-amber-100 text-amber-700'
: 'bg-slate-100 text-slate-600'
}`}>
{(product.performance_score || 0).toFixed(0)}
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{filteredProducts.length > 50 && (
<div className="px-6 py-3 bg-slate-50 border-t border-slate-100 text-center">
<p className="text-xs text-slate-400">
{filteredProducts.length} üründen ilk 50 tanesi gösteriliyor
</p>
</div>
)}
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,245 @@
import { useMemo } from 'react'
import { ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, ZAxis, Cell } from 'recharts'
import { Target, TrendingUp, AlertTriangle, Zap } from 'lucide-react'
import KpiCard from '../ui/KpiCard'
// Renk paleti - dönüşüm oranına göre
const getConversionColor = (rate) => {
if (rate >= 5) return '#10b981' // emerald
if (rate >= 2) return '#f59e0b' // amber
if (rate >= 1) return '#3b82f6' // blue
return '#94a3b8' // slate
}
const CustomTooltip = ({ active, payload }) => {
if (!active || !payload?.length) return null
const d = payload[0].payload
return (
<div className="bg-white rounded-lg shadow-lg border border-slate-200 p-3 max-w-xs">
<p className="font-semibold text-slate-900 text-sm mb-1">{d.name}</p>
<div className="space-y-0.5 text-xs text-slate-600">
<p>Ort. Görüntüleme: <span className="font-medium text-slate-900">{d.avgViews?.toLocaleString('tr-TR')}</span></p>
<p>Ürün Sayısı: <span className="font-medium text-slate-900">{d.productCount?.toLocaleString('tr-TR')}</span></p>
<p>Ort. Sipariş: <span className="font-medium text-slate-900">{d.avgOrders?.toLocaleString('tr-TR')}</span></p>
<p>Dönüşüm: <span className="font-medium text-slate-900">{d.conversionRate?.toFixed(2)}%</span></p>
</div>
<div className={`mt-2 px-2 py-0.5 rounded text-xs font-medium inline-block ${
d.quadrant === 'opportunity' ? 'bg-emerald-100 text-emerald-700' :
d.quadrant === 'saturated' ? 'bg-red-100 text-red-700' :
d.quadrant === 'niche' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{d.quadrant === 'opportunity' ? 'FIRSAT' :
d.quadrant === 'saturated' ? 'DOYGUN' :
d.quadrant === 'niche' ? 'NİŞ' : 'REKABET'}
</div>
</div>
)
}
export default function OpportunityTab({ allProducts }) {
const chartData = useMemo(() => {
if (!allProducts?.length) return { data: [], avgViews: 0, avgProducts: 0, opportunities: 0, saturated: 0 }
// Alt kategorilere göre grupla
const categoryMap = new Map()
allProducts.forEach(p => {
const cat = p.category_name || 'Bilinmeyen'
if (!categoryMap.has(cat)) {
categoryMap.set(cat, { products: [], totalViews: 0, totalOrders: 0 })
}
const c = categoryMap.get(cat)
c.products.push(p)
c.totalViews += (p.page_views || 0)
c.totalOrders += (p.orders || 0)
})
const categories = Array.from(categoryMap.entries()).map(([name, c]) => {
const productCount = c.products.length
const avgViews = productCount > 0 ? Math.round(c.totalViews / productCount) : 0
const avgOrders = productCount > 0 ? Math.round(c.totalOrders / productCount) : 0
const conversionRate = c.totalViews > 0 ? (c.totalOrders / c.totalViews) * 100 : 0
return {
name,
avgViews,
productCount,
avgOrders,
totalOrders: c.totalOrders,
conversionRate,
quadrant: '' // will be set after averages are calculated
}
})
// Ortalama hesapla (çeyrek çizgileri için)
const avgViews = categories.length > 0
? Math.round(categories.reduce((s, c) => s + c.avgViews, 0) / categories.length)
: 0
const avgProducts = categories.length > 0
? Math.round(categories.reduce((s, c) => s + c.productCount, 0) / categories.length)
: 0
// Kadranları belirle
categories.forEach(c => {
if (c.avgViews >= avgViews && c.productCount < avgProducts) {
c.quadrant = 'opportunity' // Yüksek talep, düşük arz = FIRSAT
} else if (c.avgViews < avgViews && c.productCount >= avgProducts) {
c.quadrant = 'saturated' // Düşük talep, yüksek arz = DOYGUN
} else if (c.avgViews >= avgViews && c.productCount >= avgProducts) {
c.quadrant = 'competitive' // Yüksek talep, yüksek arz = REKABET
} else {
c.quadrant = 'niche' // Düşük talep, düşük arz = NİŞ
}
})
const opportunities = categories.filter(c => c.quadrant === 'opportunity').length
const saturated = categories.filter(c => c.quadrant === 'saturated').length
return { data: categories, avgViews, avgProducts, opportunities, saturated }
}, [allProducts])
if (!allProducts?.length) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-slate-400">Fırsat haritası için veri bulunamadı</p>
</div>
)
}
return (
<div className="space-y-6">
{/* KPIs */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<KpiCard
title="Toplam Kategori"
value={chartData.data.length}
icon={Target}
color="blue"
/>
<KpiCard
title="Fırsat Alanı"
value={chartData.opportunities}
subtitle="Yüksek talep, düşük arz"
icon={Zap}
color="emerald"
/>
<KpiCard
title="Doygun Pazar"
value={chartData.saturated}
subtitle="Düşük talep, yüksek arz"
icon={AlertTriangle}
color="rose"
/>
<KpiCard
title="Ort. Görüntüleme"
value={chartData.avgViews.toLocaleString('tr-TR')}
icon={TrendingUp}
color="violet"
/>
</div>
{/* Scatter Chart */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-slate-900">Fırsat Haritası</h3>
<p className="text-xs text-slate-400 mt-1">
X: Ortalama Görüntüleme (Talep) | Y: Ürün Sayısı (Arz) | Boyut: Ort. Sipariş | Renk: Dönüşüm Oranı
</p>
</div>
<div className="h-[500px]">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 20, right: 30, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis
type="number"
dataKey="avgViews"
name="Ort. Görüntüleme"
tick={{ fill: '#64748b', fontSize: 12 }}
label={{ value: 'Ort. Görüntüleme (Talep)', position: 'insideBottom', offset: -10, fill: '#94a3b8', fontSize: 12 }}
/>
<YAxis
type="number"
dataKey="productCount"
name="Ürün Sayısı"
tick={{ fill: '#64748b', fontSize: 12 }}
label={{ value: 'Ürün Sayısı (Arz)', angle: -90, position: 'insideLeft', fill: '#94a3b8', fontSize: 12 }}
/>
<ZAxis type="number" dataKey="avgOrders" range={[50, 400]} name="Ort. Sipariş" />
<Tooltip content={<CustomTooltip />} />
<ReferenceLine
x={chartData.avgViews}
stroke="#94a3b8"
strokeDasharray="5 5"
label={{ value: 'Ort. Talep', fill: '#94a3b8', fontSize: 11 }}
/>
<ReferenceLine
y={chartData.avgProducts}
stroke="#94a3b8"
strokeDasharray="5 5"
label={{ value: 'Ort. Arz', fill: '#94a3b8', fontSize: 11 }}
/>
<Scatter data={chartData.data} name="Kategoriler">
{chartData.data.map((entry, index) => (
<Cell key={index} fill={getConversionColor(entry.conversionRate)} fillOpacity={0.7} />
))}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="flex flex-wrap gap-4 mt-4 pt-4 border-t border-slate-100">
<div className="flex items-center gap-4 text-xs text-slate-500">
<span className="font-medium text-slate-700">Kadranlar:</span>
<span className="flex items-center gap-1">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 inline-block" />
Sol Üst = FIRSAT
</span>
<span className="flex items-center gap-1">
<span className="w-2.5 h-2.5 rounded-full bg-red-500 inline-block" />
Sağ Alt = DOYGUN
</span>
<span className="flex items-center gap-1">
<span className="w-2.5 h-2.5 rounded-full bg-amber-500 inline-block" />
Sağ Üst = REKABET
</span>
<span className="flex items-center gap-1">
<span className="w-2.5 h-2.5 rounded-full bg-blue-500 inline-block" />
Sol Alt = NİŞ
</span>
</div>
</div>
</div>
{/* Opportunity List */}
{chartData.data.filter(c => c.quadrant === 'opportunity').length > 0 && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fırsat Kategorileri</h3>
<div className="space-y-2">
{chartData.data
.filter(c => c.quadrant === 'opportunity')
.sort((a, b) => b.avgViews - a.avgViews)
.map((cat, i) => (
<div key={cat.name} className="flex items-center gap-3 p-3 bg-emerald-50/50 rounded-lg">
<div className="w-7 h-7 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold shrink-0">
{i + 1}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 truncate">{cat.name}</p>
<p className="text-xs text-slate-500">
{cat.productCount} ürün · {cat.avgViews.toLocaleString('tr-TR')} ort. görüntüleme
</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-emerald-600">{cat.avgOrders.toLocaleString('tr-TR')}</p>
<p className="text-xs text-slate-400">ort. sipariş</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -1,5 +1,8 @@
import { useState } from 'react'
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ScatterChart, Scatter } from 'recharts'
import KpiCard from '../ui/KpiCard'
import { Globe, Flag, Ship, ShoppingBag } from 'lucide-react'
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
export default function OriginTab({ originAnalytics }) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: null })
@@ -65,7 +68,7 @@ export default function OriginTab({ originAnalytics }) {
console.warn('⚠️ [ORIGINTAB] No originAnalytics data - showing loading state')
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Menşei analizi yükleniyor...</p>
<p className="text-slate-400">Menşei analizi yükleniyor...</p>
</div>
)
}
@@ -116,82 +119,47 @@ export default function OriginTab({ originAnalytics }) {
<div className="space-y-6">
{/* Row 1: KPI Cards - 4 cards in a grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Card 1: Toplam Ülke Sayısı */}
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm font-medium">Toplam Ülke Sayısı</p>
<p className="text-3xl font-bold mt-2">{countries?.length || 0}</p>
</div>
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
{/* Card 2: Yerli Ürün Payı */}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm font-medium">Yerli Ürün Payı</p>
<p className="text-3xl font-bold mt-2">{kpis?.domesticPercentage || 0}%</p>
<p className="text-green-100 text-xs mt-1">{(domesticData?.count || 0).toLocaleString('tr-TR')} ürün</p>
</div>
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
</div>
</div>
</div>
{/* Card 3: İthal Ürün Payı */}
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm font-medium">İthal Ürün Payı</p>
<p className="text-3xl font-bold mt-2">{kpis?.importPercentage || 0}%</p>
<p className="text-purple-100 text-xs mt-1">{(importData?.count || 0).toLocaleString('tr-TR')} ürün</p>
</div>
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
</div>
</div>
{/* Card 4: Toplam Satış */}
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm font-medium">Toplam Satış</p>
<p className="text-3xl font-bold mt-2">{(totalOrders || 0).toLocaleString('tr-TR')}</p>
<p className="text-orange-100 text-xs mt-1">Tüm ülkeler</p>
</div>
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
</div>
</div>
</div>
<KpiCard
title="Toplam Ülke Sayısı"
value={countries?.length || 0}
icon={Globe}
color="blue"
/>
<KpiCard
title="Yerli Ürün Payı"
value={`${kpis?.domesticPercentage || 0}%`}
subtitle={`${(domesticData?.count || 0).toLocaleString('tr-TR')} ürün`}
icon={Flag}
color="emerald"
/>
<KpiCard
title="İthal Ürün Payı"
value={`${kpis?.importPercentage || 0}%`}
subtitle={`${(importData?.count || 0).toLocaleString('tr-TR')} ürün`}
icon={Ship}
color="violet"
/>
<KpiCard
title="Toplam Satış"
value={(totalOrders || 0).toLocaleString('tr-TR')}
subtitle="Tüm ülkeler"
icon={ShoppingBag}
color="orange"
/>
</div>
{/* Row 2: Kategori-Ülke Isı Haritası (Top 10x10) - Full Width */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-800 mb-4">
Kategori-Ülke Isı Haritası (Top 10x10)
</h3>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr>
<th className="border border-gray-200 bg-gray-50 p-2 text-left font-semibold text-gray-700 sticky left-0 z-10">Kategori</th>
<th className="border border-slate-200 bg-slate-50 p-2 text-left font-semibold text-slate-700 sticky left-0 z-10">Kategori</th>
{topCountriesForHeatmap.map(country => (
<th key={country} className="border border-gray-200 bg-gray-50 p-2 text-center font-semibold text-gray-700 min-w-[100px]">
<th key={country} className="border border-slate-200 bg-slate-50 p-2 text-center font-semibold text-slate-700 min-w-[100px]">
{country}
</th>
))}
@@ -200,7 +168,7 @@ export default function OriginTab({ originAnalytics }) {
<tbody>
{topCategories.map(category => (
<tr key={category}>
<td className="border border-gray-200 p-2 font-medium text-gray-700 bg-gray-50 sticky left-0 z-10">{category}</td>
<td className="border border-slate-200 p-2 font-medium text-slate-700 bg-slate-50 sticky left-0 z-10">{category}</td>
{topCountriesForHeatmap.map(country => {
const cellData = categoryCountryMatrix[category]?.[country]
const count = cellData?.count || 0
@@ -217,7 +185,7 @@ export default function OriginTab({ originAnalytics }) {
else if (count > 0) bgColor = 'bg-blue-50'
return (
<td key={country} className={`border border-gray-200 p-2 text-center ${bgColor}`}>
<td key={country} className={`border border-slate-200 p-2 text-center ${bgColor}`}>
{count > 0 ? (
<div>
<div className="text-xs font-semibold">Satış: {orders.toLocaleString('tr-TR')}</div>
@@ -238,26 +206,26 @@ export default function OriginTab({ originAnalytics }) {
</div>
{/* Row 3: En Çok Satan Ülkeler (Top 20) - Full Width, 10x10 Grid */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-800 mb-4">
En Çok Satan Ülkeler (Top 20)
</h3>
<div className="grid grid-cols-2 gap-4">
{/* Left Column: Countries 1-10 */}
<div className="space-y-2">
{(topByOrders || []).slice(0, 10).map((item, index) => (
<div key={item.country} className="flex items-center justify-between p-3 bg-gradient-to-r from-blue-50 to-transparent rounded-lg hover:from-blue-100 transition-colors">
<div key={item.country} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-orange-50/30 transition-colors">
<div className="flex items-center gap-3 flex-1">
<span className="flex items-center justify-center w-8 h-8 bg-blue-500 text-white rounded-full font-bold text-sm">
<span className="flex items-center justify-center w-8 h-8 bg-orange-500 text-white rounded-full font-bold text-sm">
{index + 1}
</span>
<span className="font-semibold text-gray-800">{item.country}</span>
<span className="font-semibold text-slate-800">{item.country}</span>
</div>
<div className="text-right space-y-1">
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Satış:</span> {item.totalOrders.toLocaleString('tr-TR')}
</p>
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Ciro:</span> {item.totalRevenue.toLocaleString('tr-TR')}
</p>
</div>
@@ -268,18 +236,18 @@ export default function OriginTab({ originAnalytics }) {
{/* Right Column: Countries 11-20 */}
<div className="space-y-2">
{(topByOrders || []).slice(10, 20).map((item, index) => (
<div key={item.country} className="flex items-center justify-between p-3 bg-gradient-to-r from-purple-50 to-transparent rounded-lg hover:from-purple-100 transition-colors">
<div key={item.country} className="flex items-center justify-between p-3 bg-gradient-to-r from-purple-50 to-transparent rounded-xl hover:from-purple-100 transition-colors">
<div className="flex items-center gap-3 flex-1">
<span className="flex items-center justify-center w-8 h-8 bg-purple-500 text-white rounded-full font-bold text-sm">
{index + 11}
</span>
<span className="font-semibold text-gray-800">{item.country}</span>
<span className="font-semibold text-slate-800">{item.country}</span>
</div>
<div className="text-right space-y-1">
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Satış:</span> {item.totalOrders.toLocaleString('tr-TR')}
</p>
<p className="text-sm text-gray-900">
<p className="text-sm text-slate-900">
<span className="font-semibold">Ciro:</span> {item.totalRevenue.toLocaleString('tr-TR')}
</p>
</div>
@@ -292,8 +260,8 @@ export default function OriginTab({ originAnalytics }) {
{/* Row 4: Two Charts Side by Side (50%-50%) */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Chart 1: Ülke Bazlı Satış (Bar Chart) */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-800 mb-4">
Ülke Bazlı Satış (Top 15)
</h3>
<ResponsiveContainer width="100%" height={400}>
@@ -308,22 +276,17 @@ export default function OriginTab({ originAnalytics }) {
/>
<YAxis tick={{ fontSize: 11 }} />
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
fontSize: '12px'
}}
{...CHART_TOOLTIP_STYLE}
formatter={(value) => value.toLocaleString('tr-TR')}
/>
<Bar dataKey="totalOrders" fill="#3b82f6" radius={[8, 8, 0, 0]} />
<Bar dataKey="totalOrders" fill={CHART_COLORS[0]} radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
{/* Chart 2: Ortalama Fiyat / Ciro İlişkisi (Scatter Chart) */}
<div className="bg-white rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
<div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-slate-800 mb-4">
Ciro / Satış İlişkisi (Top 15)
</h3>
<ResponsiveContainer width="100%" height={400}>
@@ -349,12 +312,12 @@ export default function OriginTab({ originAnalytics }) {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3">
<p className="font-semibold text-gray-900 mb-2 text-sm">{data.name}</p>
<p className="font-semibold text-gray-900 text-sm">
<div className="bg-slate-800 border-none rounded-xl shadow-lg p-3">
<p className="font-semibold text-slate-50 mb-2 text-sm">{data.name}</p>
<p className="font-semibold text-slate-200 text-sm">
Ciro: {data.totalRevenue.toLocaleString('tr-TR')}
</p>
<p className="font-semibold text-gray-900 text-sm">
<p className="font-semibold text-slate-200 text-sm">
Satış: {data.totalOrders.toLocaleString('tr-TR')}
</p>
</div>
@@ -377,70 +340,70 @@ export default function OriginTab({ originAnalytics }) {
</div>
{/* Row 5: Detaylı Ülke Karşılaştırma Tablosu */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Ülke Karşılaştırma</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Ülke Karşılaştırma</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b-2 border-gray-200 bg-gray-50">
<thead className="border-b-2 border-slate-200 bg-slate-50">
<tr>
<th
className="px-4 py-3 text-left text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-left text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('country')}
>
Ülke{renderSortIndicator('country')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('count')}
>
Ürün{renderSortIndicator('count')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('totalOrders')}
>
Satış{renderSortIndicator('totalOrders')}
</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('totalRevenue')}
>
Ciro{renderSortIndicator('totalRevenue')}
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">Pay %</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Pay %</th>
<th
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('avgPrice')}
>
Ort. Fiyat{renderSortIndicator('avgPrice')}
</th>
<th
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('categoryCount')}
>
Kategori{renderSortIndicator('categoryCount')}
</th>
<th
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
onClick={() => handleSort('brandCount')}
>
Marka{renderSortIndicator('brandCount')}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tbody className="divide-y divide-slate-100">
{getSortedData().map((item) => (
<tr key={item.country} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm font-semibold text-gray-900">
<tr key={item.country} className="hover:bg-orange-50/30">
<td className="px-4 py-3 text-sm font-semibold text-slate-900">
{item.country}
</td>
<td className="px-4 py-3 text-sm text-right text-gray-900">
<td className="px-4 py-3 text-sm text-right text-slate-900">
{item.count.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-sm text-right font-semibold text-gray-900">
<td className="px-4 py-3 text-sm text-right font-semibold text-slate-900">
{item.totalOrders.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-sm text-right font-semibold text-gray-900">
<td className="px-4 py-3 text-sm text-right font-semibold text-slate-900">
{item.totalRevenue.toLocaleString('tr-TR')}
</td>
<td className="px-4 py-3 text-sm text-right">
@@ -448,11 +411,11 @@ export default function OriginTab({ originAnalytics }) {
{((item.totalOrders / totalOrders) * 100).toFixed(1)}%
</span>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900">
<td className="px-4 py-3 text-right text-sm text-slate-900">
{item.avgPrice.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}
</td>
<td className="px-4 py-3 text-center text-sm text-gray-900">{item.categoryCount || 0}</td>
<td className="px-4 py-3 text-center text-sm text-gray-900">{item.brandCount || 0}</td>
<td className="px-4 py-3 text-center text-sm text-slate-900">{item.categoryCount || 0}</td>
<td className="px-4 py-3 text-center text-sm text-slate-900">{item.brandCount || 0}</td>
</tr>
))}
</tbody>

View File

@@ -1,109 +1,381 @@
import { useState, useEffect, useMemo } from 'react'
import { Package, ShoppingCart, Eye, DollarSign, Tag, TrendingUp, Swords } from 'lucide-react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell } from 'recharts'
import KpiCard from '../ui/KpiCard'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../../config/api'
// Competition Score gauge component
function CompetitionGauge({ score }) {
const radius = 60
const circumference = Math.PI * radius
const offset = circumference - (score / 100) * circumference
const color = score >= 67 ? '#ef4444' : score >= 34 ? '#f59e0b' : '#22c55e'
const label = score >= 67 ? 'Yoğun Rekabet' : score >= 34 ? 'Orta Rekabet' : 'Düşük Rekabet (Fırsat)'
const bgColor = score >= 67 ? 'bg-red-50 text-red-700' : score >= 34 ? 'bg-amber-50 text-amber-700' : 'bg-emerald-50 text-emerald-700'
return (
<div className="flex flex-col items-center">
<svg width="140" height="80" viewBox="0 0 140 80">
<path
d="M 10 75 A 60 60 0 0 1 130 75"
fill="none"
stroke="#e2e8f0"
strokeWidth="10"
strokeLinecap="round"
/>
<path
d="M 10 75 A 60 60 0 0 1 130 75"
fill="none"
stroke={color}
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
style={{ transition: 'stroke-dashoffset 1s ease' }}
/>
<text x="70" y="65" textAnchor="middle" className="text-2xl font-bold" fill="#1e293b">
{Math.round(score)}
</text>
</svg>
<span className={`text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${bgColor}`}>
{label}
</span>
</div>
)
}
export default function OverviewTab({
overviewKPIs,
topSellingProducts,
topSellingBrands,
topSellingCategories,
mostViewedCategories
mostViewedCategories,
reportId,
allProducts
}) {
// Sales Funnel state
const [salesData, setSalesData] = useState(null)
const [salesLoading, setSalesLoading] = useState(false)
// Fetch sales analytics
useEffect(() => {
if (!reportId) return
setSalesLoading(true)
fetchWithTimeout(
`${API_URL}/api/reports/${reportId}/sales-analytics`,
{},
TIMEOUT_CONFIG.DASHBOARD
)
.then(res => {
if (!res.ok) throw new Error('Sales data failed')
return res.json()
})
.then(data => setSalesData(data))
.catch(() => {}) // silently fail - optional feature
.finally(() => setSalesLoading(false))
}, [reportId])
// Price Distribution histogram
const priceDistribution = useMemo(() => {
if (!allProducts?.length) return null
const prices = allProducts.map(p => p.price || 0).filter(p => p > 0)
if (prices.length === 0) return null
const min = Math.min(...prices)
const max = Math.max(...prices)
const mean = prices.reduce((s, p) => s + p, 0) / prices.length
const sortedPrices = [...prices].sort((a, b) => a - b)
const median = sortedPrices.length % 2 === 0
? (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
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++
})
return { buckets, mean: Math.round(mean), median: Math.round(median) }
}, [allProducts])
// Competition Score
const competitionScore = useMemo(() => {
if (!allProducts?.length) return null
const products = allProducts
const totalProducts = products.length
const uniqueBrands = new Set(products.map(p => p.brand).filter(Boolean)).size
// Brand diversity: unique_brands / total_products * 30
const brandDiversity = Math.min(30, (uniqueBrands / totalProducts) * 30)
// Price variance: std_dev / mean * 20
const prices = products.map(p => p.price || 0).filter(p => p > 0)
const meanPrice = prices.length > 0 ? prices.reduce((s, p) => s + p, 0) / prices.length : 0
const variance = prices.length > 0
? prices.reduce((s, p) => s + Math.pow(p - meanPrice, 2), 0) / prices.length
: 0
const stdDev = Math.sqrt(variance)
const priceVariance = meanPrice > 0 ? Math.min(20, (stdDev / meanPrice) * 20) : 0
// Product density: log(total_products) * 10
const productDensity = Math.min(10, Math.log10(Math.max(1, totalProducts)) * 10)
// HHI inverse: (1 - HHI/10000) * 40
const brandOrders = {}
const totalOrders = products.reduce((s, p) => {
const b = p.brand || 'Unknown'
brandOrders[b] = (brandOrders[b] || 0) + (p.orders || 0)
return s + (p.orders || 0)
}, 0)
let hhi = 0
if (totalOrders > 0) {
Object.values(brandOrders).forEach(orders => {
const share = (orders / totalOrders) * 100
hhi += share * share
})
}
const hhiInverse = Math.max(0, (1 - hhi / 10000) * 40)
const score = Math.min(100, Math.max(0, brandDiversity + priceVariance + productDensity + hhiInverse))
return { score, brandDiversity, priceVariance, productDensity, hhiInverse, hhi: Math.round(hhi) }
}, [allProducts])
if (!overviewKPIs) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Genel bakış verileri yükleniyor...</p>
<p className="text-slate-400">Genel bakış verileri yükleniyor...</p>
</div>
)
}
// Sales funnel data
const funnelData = salesData ? [
{ name: 'Görüntüleme', value: salesData.total_views || salesData.totalViews || 0, color: '#6366f1' },
{ name: 'Sepet', value: salesData.total_baskets || salesData.totalBaskets || 0, color: '#f59e0b' },
{ name: 'Sipariş', value: salesData.total_orders || salesData.totalOrders || 0, color: '#22c55e' },
] : null
const conversionRates = salesData ? {
viewToBasket: salesData.view_to_basket_rate || salesData.viewToBasketRate || 0,
basketToOrder: salesData.basket_to_order_rate || salesData.basketToOrderRate || 0,
viewToOrder: salesData.view_to_order_rate || salesData.viewToOrderRate || 0,
} : null
return (
<div className="space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm font-medium">Toplam Ürün</p>
<p className="text-3xl font-bold mt-2">
{overviewKPIs.totalProducts.toLocaleString('tr-TR')}
</p>
</div>
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
</div>
<KpiCard
title="Toplam Ürün"
value={overviewKPIs.totalProducts.toLocaleString('tr-TR')}
icon={Package}
color="blue"
/>
<KpiCard
title="Toplam Satın Alma"
value={overviewKPIs.totalOrders.toLocaleString('tr-TR')}
icon={ShoppingCart}
color="emerald"
/>
<KpiCard
title="Toplam Görüntülenme"
value={overviewKPIs.totalViews.toLocaleString('tr-TR')}
icon={Eye}
color="violet"
/>
<KpiCard
title="Toplam Ciro"
value={`${(overviewKPIs.totalRevenue || 0).toLocaleString('tr-TR')}`}
icon={DollarSign}
color="orange"
/>
<KpiCard
title="Ortalama Fiyat"
value={`${overviewKPIs.avgPrice.toLocaleString('tr-TR')}`}
icon={Tag}
color="rose"
/>
</div>
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-green-100 text-sm font-medium">Toplam Satın Alma</p>
<p className="text-3xl font-bold mt-2">
{overviewKPIs.totalOrders.toLocaleString('tr-TR')}
{/* Competition Score */}
{competitionScore && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col items-center justify-center">
<h3 className="text-sm font-medium text-slate-500 mb-3">Rekabet Skoru</h3>
<CompetitionGauge score={competitionScore.score} />
<p className="text-xs text-slate-400 mt-3 text-center">
HHI: {competitionScore.hhi.toLocaleString('tr-TR')}
</p>
</div>
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-sm font-medium text-slate-500 mb-4">Rekabet Bileşenleri</h3>
<div className="space-y-3">
{[
{ label: 'Marka Çeşitliliği', value: competitionScore.brandDiversity, max: 30, color: 'bg-blue-500' },
{ label: 'Fiyat Varyansı', value: competitionScore.priceVariance, max: 20, color: 'bg-amber-500' },
{ label: 'Ürün Yoğunluğu', value: competitionScore.productDensity, max: 10, color: 'bg-violet-500' },
{ label: 'HHI Ters (Dağılım)', value: competitionScore.hhiInverse, max: 40, color: 'bg-emerald-500' },
].map(item => (
<div key={item.label}>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-slate-600">{item.label}</span>
<span className="text-xs font-medium text-slate-900">{item.value.toFixed(1)} / {item.max}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className={`${item.color} h-2 rounded-full transition-all`}
style={{ width: `${(item.value / item.max) * 100}%` }}
/>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Sales Funnel */}
{funnelData && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Satış Hunisi</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Funnel Bar Chart */}
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={funnelData} layout="vertical" margin={{ left: 20, right: 30 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" horizontal={false} />
<XAxis type="number" tick={{ fill: '#64748b', fontSize: 12 }} />
<YAxis dataKey="name" type="category" tick={{ fill: '#64748b', fontSize: 12 }} width={90} />
<Tooltip
formatter={(value) => [value.toLocaleString('tr-TR'), '']}
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0' }}
/>
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
{funnelData.map((entry, index) => (
<Cell key={index} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm font-medium">Toplam Görüntülenme</p>
<p className="text-3xl font-bold mt-2">
{overviewKPIs.totalViews.toLocaleString('tr-TR')}
</p>
{/* Conversion Rates */}
{conversionRates && (
<div className="flex flex-col justify-center space-y-4">
<div className="flex items-center gap-3 p-3 bg-indigo-50/50 rounded-lg">
<div className="w-2 h-2 rounded-full bg-indigo-500" />
<span className="text-sm text-slate-600 flex-1">Görüntüleme Sepet</span>
<span className="text-sm font-bold text-indigo-600">
%{typeof conversionRates.viewToBasket === 'number' ? conversionRates.viewToBasket.toFixed(2) : conversionRates.viewToBasket}
</span>
</div>
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<div className="flex items-center gap-3 p-3 bg-amber-50/50 rounded-lg">
<div className="w-2 h-2 rounded-full bg-amber-500" />
<span className="text-sm text-slate-600 flex-1">Sepet Sipariş</span>
<span className="text-sm font-bold text-amber-600">
%{typeof conversionRates.basketToOrder === 'number' ? conversionRates.basketToOrder.toFixed(2) : conversionRates.basketToOrder}
</span>
</div>
<div className="flex items-center gap-3 p-3 bg-emerald-50/50 rounded-lg">
<div className="w-2 h-2 rounded-full bg-emerald-500" />
<span className="text-sm text-slate-600 flex-1">Görüntüleme Sipariş</span>
<span className="text-sm font-bold text-emerald-600">
%{typeof conversionRates.viewToOrder === 'number' ? conversionRates.viewToOrder.toFixed(2) : conversionRates.viewToOrder}
</span>
</div>
</div>
)}
</div>
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm font-medium">Toplam Ciro</p>
<p className="text-3xl font-bold mt-2">
{(overviewKPIs.totalRevenue || 0).toLocaleString('tr-TR')}
</p>
</div>
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/* Top Conversion Products */}
{salesData?.top_conversion_products?.length > 0 && (
<div className="mt-6 pt-4 border-t border-slate-100">
<h4 className="text-sm font-medium text-slate-700 mb-3">En Yüksek Dönüşüm Oranı</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{salesData.top_conversion_products.slice(0, 10).map((p, i) => (
<div key={i} className="flex items-center gap-2 p-2 bg-slate-50 rounded-lg text-sm">
<span className="w-5 h-5 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold shrink-0">{i + 1}</span>
<span className="truncate flex-1 text-slate-700">{p.name}</span>
<span className="font-medium text-emerald-600 shrink-0">%{(p.conversion_rate || 0).toFixed(1)}</span>
</div>
))}
</div>
</div>
)}
<div className="bg-gradient-to-br from-red-500 to-red-600 rounded-lg shadow-sm p-6 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-red-100 text-sm font-medium">Ortalama Fiyat</p>
<p className="text-3xl font-bold mt-2">
{overviewKPIs.avgPrice.toLocaleString('tr-TR')}
{/* Top Performance Products */}
{salesData?.top_performance_products?.length > 0 && (
<div className="mt-4 pt-4 border-t border-slate-100">
<h4 className="text-sm font-medium text-slate-700 mb-3">En Yüksek Performans Skoru</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{salesData.top_performance_products.slice(0, 10).map((p, i) => (
<div key={i} className="flex items-center gap-2 p-2 bg-slate-50 rounded-lg text-sm">
<span className="w-5 h-5 bg-orange-500 text-white rounded-full flex items-center justify-center text-xs font-bold shrink-0">{i + 1}</span>
<span className="truncate flex-1 text-slate-700">{p.name}</span>
<span className="font-medium text-orange-600 shrink-0">{(p.performance_score || 0).toFixed(0)}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Price Distribution */}
{priceDistribution && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-1">Fiyat Dağılımı</h3>
<p className="text-xs text-slate-400 mb-4">
Ort: {priceDistribution.mean.toLocaleString('tr-TR')} · Medyan: {priceDistribution.median.toLocaleString('tr-TR')}
</p>
</div>
<div className="bg-red-400 bg-opacity-30 rounded-full p-3">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
</div>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={priceDistribution.buckets} margin={{ top: 10, right: 30, left: 10, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" vertical={false} />
<XAxis
dataKey="range"
tick={{ fill: '#64748b', fontSize: 10 }}
angle={-30}
textAnchor="end"
height={60}
/>
<YAxis tick={{ fill: '#64748b', fontSize: 12 }} />
<Tooltip
formatter={(value) => [`${value} ürün`, 'Sayı']}
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0' }}
/>
<ReferenceLine
x={priceDistribution.buckets.findIndex(b => b.min <= priceDistribution.mean && b.max > priceDistribution.mean)}
stroke="#f97316"
strokeDasharray="5 5"
label={{ value: `Ort: ₺${priceDistribution.mean}`, fill: '#f97316', fontSize: 11, position: 'top' }}
/>
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} label={{ position: 'top', fill: '#64748b', fontSize: 11 }} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Row 2: Top Products & Top Brands */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* En Çok Satış Yapan Ürünler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Ürünler</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Ürünler</h3>
<div className="space-y-3">
{topSellingProducts.map((product, index) => (
<a
@@ -111,9 +383,9 @@ export default function OverviewTab({
href={product.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
>
<div className="flex-shrink-0 w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
<div className="flex-shrink-0 w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
{index + 1}
</div>
<img
@@ -122,12 +394,12 @@ export default function OverviewTab({
className="w-16 h-16 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{product.name}</p>
<p className="text-xs text-gray-600">{product.price?.toLocaleString('tr-TR')}</p>
<p className="text-sm font-medium text-slate-900 truncate">{product.name}</p>
<p className="text-xs text-slate-500">{product.price?.toLocaleString('tr-TR')}</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-gray-900">{product.orders?.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">satış</p>
<p className="text-sm font-bold text-slate-900">{product.orders?.toLocaleString('tr-TR')}</p>
<p className="text-xs text-slate-500">satış</p>
</div>
</a>
))}
@@ -135,8 +407,8 @@ export default function OverviewTab({
</div>
{/* En Çok Satış Yapan Marka */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Marka</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Marka</h3>
<div className="space-y-3">
{topSellingBrands.map((brand, index) => (
<a
@@ -144,17 +416,17 @@ export default function OverviewTab({
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
>
<div className="flex-shrink-0 w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
{index + 1}
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{brand.name}</p>
<p className="text-sm font-medium text-slate-900">{brand.name}</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-gray-900">{brand.totalOrders.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">toplam satış</p>
<p className="text-sm font-bold text-slate-900">{brand.totalOrders.toLocaleString('tr-TR')}</p>
<p className="text-xs text-slate-500">toplam satış</p>
</div>
</a>
))}
@@ -165,8 +437,8 @@ export default function OverviewTab({
{/* Row 3: Top Categories by Revenue & Most Viewed Categories */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* En Çok Satış Yapan Kategoriler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Kategoriler</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Kategoriler</h3>
<div className="space-y-3">
{topSellingCategories.map((category, index) => (
<a
@@ -174,17 +446,17 @@ export default function OverviewTab({
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
>
<div className="flex-shrink-0 w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
{index + 1}
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{category.name}</p>
<p className="text-sm font-medium text-slate-900">{category.name}</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-gray-900">{category.revenue.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">toplam ciro</p>
<p className="text-sm font-bold text-slate-900">{category.revenue.toLocaleString('tr-TR')}</p>
<p className="text-xs text-slate-500">toplam ciro</p>
</div>
</a>
))}
@@ -192,8 +464,8 @@ export default function OverviewTab({
</div>
{/* En Çok Görüntülenme Alan Kategoriler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Görüntülenme Alan Kategoriler</h3>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Görüntülenme Alan Kategoriler</h3>
<div className="space-y-3">
{mostViewedCategories.map((category, index) => (
<a
@@ -201,17 +473,17 @@ export default function OverviewTab({
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
>
<div className="flex-shrink-0 w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
{index + 1}
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{category.name}</p>
<p className="text-sm font-medium text-slate-900">{category.name}</p>
</div>
<div className="text-right">
<p className="text-sm font-bold text-gray-900">{category.views.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">görüntülenme</p>
<p className="text-sm font-bold text-slate-900">{category.views.toLocaleString('tr-TR')}</p>
<p className="text-xs text-slate-500">görüntülenme</p>
</div>
</a>
))}

View File

@@ -1,4 +1,5 @@
import { useState, useMemo, useCallback, useEffect } from 'react'
import { Search, Star, X, Filter, SlidersHorizontal } from 'lucide-react'
export default function ProductFinderTab({ allProducts }) {
@@ -233,14 +234,14 @@ export default function ProductFinderTab({ allProducts }) {
if (!allProducts || allProducts.length === 0) {
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<div className="text-gray-400 mb-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 text-center">
<div className="text-slate-400 mb-4">
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Henüz Ürün Yok</h3>
<p className="text-gray-500">Ürün verisi yüklendiğinde burada görüntülenecektir.</p>
<h3 className="text-lg font-medium text-slate-900 mb-2">Henüz Ürün Yok</h3>
<p className="text-slate-400">Ürün verisi yüklendiğinde burada görüntülenecektir.</p>
</div>
)
}
@@ -248,70 +249,68 @@ export default function ProductFinderTab({ allProducts }) {
return (
<div className="space-y-6">
{/* Quick Filters */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<h3 className="text-sm font-semibold text-slate-700 mb-3 flex items-center gap-2">
<span></span>
Hızlı Filtreler
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<button
onClick={() => applyQuickFilter({ minOrders: '100', minPrice: '', maxPrice: '' })}
className="p-3 rounded-lg border-2 border-green-500 bg-green-50 transition-all duration-200 hover:shadow-md hover:scale-105"
className="p-3 rounded-xl border-2 border-green-500 bg-green-50 transition-all duration-200 hover:shadow-md hover:scale-105"
>
<div className="text-2xl mb-1">🔥</div>
<div className="text-xs font-semibold text-gray-800">Çok Satanlar</div>
<div className="text-xs text-gray-500 mt-1">(100+ satış)</div>
<div className="text-xs font-semibold text-slate-800">Çok Satanlar</div>
<div className="text-xs text-slate-400 mt-1">(100+ satış)</div>
</button>
<button
onClick={() => applyQuickFilter({ minViews: '5000', minPrice: '', maxPrice: '' })}
className="p-3 rounded-lg border-2 border-purple-500 bg-purple-50 transition-all duration-200 hover:shadow-md hover:scale-105"
className="p-3 rounded-xl border-2 border-purple-500 bg-purple-50 transition-all duration-200 hover:shadow-md hover:scale-105"
>
<div className="text-2xl mb-1">👁</div>
<div className="text-xs font-semibold text-gray-800">Yüksek Görüntüleme</div>
<div className="text-xs text-gray-500 mt-1">(5K+ görüntülenme)</div>
<div className="text-xs font-semibold text-slate-800">Yüksek Görüntüleme</div>
<div className="text-xs text-slate-400 mt-1">(5K+ görüntülenme)</div>
</button>
<button
onClick={() => applyQuickFilter({ minPrice: '1000', maxPrice: '', minOrders: '', minViews: '' })}
className="p-3 rounded-lg border-2 border-orange-500 bg-orange-50 transition-all duration-200 hover:shadow-md hover:scale-105"
className="p-3 rounded-xl border-2 border-orange-500 bg-orange-50 transition-all duration-200 hover:shadow-md hover:scale-105"
>
<div className="text-2xl mb-1">💎</div>
<div className="text-xs font-semibold text-gray-800">Premium</div>
<div className="text-xs text-gray-500 mt-1">(1000+ TL)</div>
<div className="text-xs font-semibold text-slate-800">Premium</div>
<div className="text-xs text-slate-400 mt-1">(1000+ TL)</div>
</button>
<button
onClick={() => applyQuickFilter({ maxPrice: '200', minPrice: '', minOrders: '', minViews: '' })}
className="p-3 rounded-lg border-2 border-blue-500 bg-blue-50 transition-all duration-200 hover:shadow-md hover:scale-105"
className="p-3 rounded-xl border-2 border-blue-500 bg-blue-50 transition-all duration-200 hover:shadow-md hover:scale-105"
>
<div className="text-2xl mb-1">💰</div>
<div className="text-xs font-semibold text-gray-800">Ekonomik</div>
<div className="text-xs text-gray-500 mt-1">(&lt;200 TL)</div>
<div className="text-xs font-semibold text-slate-800">Ekonomik</div>
<div className="text-xs text-slate-400 mt-1">(&lt;200 TL)</div>
</button>
<button
onClick={() => applyQuickFilter({ minReviews: '100', minPrice: '', maxPrice: '', minOrders: '', minViews: '' })}
className="p-3 rounded-lg border-2 border-red-500 bg-red-50 transition-all duration-200 hover:shadow-md hover:scale-105"
className="p-3 rounded-xl border-2 border-red-500 bg-red-50 transition-all duration-200 hover:shadow-md hover:scale-105"
>
<div className="text-2xl mb-1">💬</div>
<div className="text-xs font-semibold text-gray-800">Çok Yorumlananlar</div>
<div className="text-xs text-gray-500 mt-1">(100+ yorum)</div>
<div className="text-xs font-semibold text-slate-800">Çok Yorumlananlar</div>
<div className="text-xs text-slate-400 mt-1">(100+ yorum)</div>
</button>
<button
onClick={() => applyQuickFilter({ minRating: '4.5', minPrice: '', maxPrice: '', minOrders: '', minViews: '', minReviews: '' })}
className="p-3 rounded-lg border-2 border-indigo-500 bg-indigo-50 transition-all duration-200 hover:shadow-md hover:scale-105"
className="p-3 rounded-xl border-2 border-indigo-500 bg-indigo-50 transition-all duration-200 hover:shadow-md hover:scale-105"
>
<div className="text-2xl mb-1"></div>
<div className="text-xs font-semibold text-gray-800">Yüksek Puanlılar</div>
<div className="text-xs text-gray-500 mt-1">(4.5+ puan)</div>
<div className="text-xs font-semibold text-slate-800">Yüksek Puanlılar</div>
<div className="text-xs text-slate-400 mt-1">(4.5+ puan)</div>
</button>
</div>
{activeFilterCount > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="mt-3 pt-3 border-t border-slate-200">
<button
onClick={clearFilters}
className="w-full px-4 py-2 bg-red-50 text-red-700 rounded-xl hover:bg-red-100 transition-colors border border-red-200 flex items-center justify-center gap-2"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<X size={16} />
Filtreleri Temizle ({activeFilterCount})
</button>
</div>
@@ -319,31 +318,31 @@ export default function ProductFinderTab({ allProducts }) {
</div>
{/* Filter Panel */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Search */}
<div className="lg:col-span-2">
<label className="block text-xs font-medium text-gray-600 mb-2">
🔎 Ürün, Marka veya Barkod Ara
<label className="block text-xs font-medium text-slate-500 mb-2 flex items-center gap-1">
<Search size={12} /> Ürün, Marka veya Barkod Ara
</label>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Örn: iPhone, Samsung, 8690000000000"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Category */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
📁 Kategori
</label>
<select
value={filters.category}
onChange={(e) => updateFilter('category', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
>
<option value="">Tüm Kategoriler</option>
{uniqueCategories.map(cat => (
@@ -354,13 +353,13 @@ export default function ProductFinderTab({ allProducts }) {
{/* Brand */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
🏷 Marka
</label>
<select
value={filters.brand}
onChange={(e) => updateFilter('brand', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
>
<option value="">Tüm Markalar</option>
{uniqueBrands.map(brand => (
@@ -371,7 +370,7 @@ export default function ProductFinderTab({ allProducts }) {
{/* Min Price */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
💵 Min Fiyat (TL)
</label>
<input
@@ -380,13 +379,13 @@ export default function ProductFinderTab({ allProducts }) {
onChange={(e) => updateFilter('minPrice', e.target.value)}
placeholder="0"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Max Price */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
💵 Max Fiyat (TL)
</label>
<input
@@ -395,19 +394,19 @@ export default function ProductFinderTab({ allProducts }) {
onChange={(e) => updateFilter('maxPrice', e.target.value)}
placeholder="∞"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Country */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
🌍 Ülke/Menşei
</label>
<select
value={filters.country}
onChange={(e) => updateFilter('country', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
>
<option value="">Tüm Ülkeler</option>
{uniqueCountries.map(country => (
@@ -418,7 +417,7 @@ export default function ProductFinderTab({ allProducts }) {
{/* Min Orders */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
🛒 Min Satış (Son 3 Gün)
</label>
<input
@@ -427,13 +426,13 @@ export default function ProductFinderTab({ allProducts }) {
onChange={(e) => updateFilter('minOrders', e.target.value)}
placeholder="0"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Min Views */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
👁 Min Görüntülenme
</label>
<input
@@ -442,13 +441,13 @@ export default function ProductFinderTab({ allProducts }) {
onChange={(e) => updateFilter('minViews', e.target.value)}
placeholder="0"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Min Reviews */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
💬 Min Yorum Sayısı
</label>
<input
@@ -457,13 +456,13 @@ export default function ProductFinderTab({ allProducts }) {
onChange={(e) => updateFilter('minReviews', e.target.value)}
placeholder="0"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Max Reviews */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
💬 Max Yorum Sayısı
</label>
<input
@@ -472,13 +471,13 @@ export default function ProductFinderTab({ allProducts }) {
onChange={(e) => updateFilter('maxReviews', e.target.value)}
placeholder="∞"
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
{/* Min Rating */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
<label className="block text-xs font-medium text-slate-500 mb-2">
Min Puan
</label>
<input
@@ -489,21 +488,21 @@ export default function ProductFinderTab({ allProducts }) {
min="0"
max="5"
step="0.5"
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
/>
</div>
</div>
{/* Sort Controls */}
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="mt-4 pt-4 border-t border-slate-200">
<div className="flex items-center gap-4 flex-wrap">
<label className="text-xs font-medium text-gray-600">
<label className="text-xs font-medium text-slate-500">
Sıralama:
</label>
<select
value={filters.sortBy}
onChange={(e) => updateFilter('sortBy', e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="px-3 py-1.5 border border-slate-300 rounded-xl text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
>
<option value="orders">Satış Sayısı</option>
<option value="views">Görüntülenme</option>
@@ -514,7 +513,7 @@ export default function ProductFinderTab({ allProducts }) {
<select
value={filters.sortOrder}
onChange={(e) => updateFilter('sortOrder', e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="px-3 py-1.5 border border-slate-300 rounded-xl text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
>
<option value="desc">Azalan (Yüksek Düşük)</option>
<option value="asc">Artan (Düşük Yüksek)</option>
@@ -525,73 +524,73 @@ export default function ProductFinderTab({ allProducts }) {
{/* Active Filters */}
{activeFilterCount > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-blue-900">Aktif Filtreler:</span>
<span className="text-sm font-medium text-orange-900">Aktif Filtreler:</span>
{filters.searchText && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Arama: "{filters.searchText}"
<button onClick={() => setSearchText('')} className="hover:text-blue-900">×</button>
<button onClick={() => setSearchText('')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.category && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Kategori: {filters.category}
<button onClick={() => updateFilter('category', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('category', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.brand && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Marka: {filters.brand}
<button onClick={() => updateFilter('brand', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('brand', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.country && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Ülke: {filters.country}
<button onClick={() => updateFilter('country', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('country', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.minPrice && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Min: {parseFloat(filters.minPrice).toLocaleString('tr-TR')}
<button onClick={() => updateFilter('minPrice', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('minPrice', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.maxPrice && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Max: {parseFloat(filters.maxPrice).toLocaleString('tr-TR')}
<button onClick={() => updateFilter('maxPrice', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('maxPrice', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.minOrders && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Min Satış: {parseInt(filters.minOrders).toLocaleString('tr-TR')}
<button onClick={() => updateFilter('minOrders', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('minOrders', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.minViews && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Min Görüntülenme: {parseInt(filters.minViews).toLocaleString('tr-TR')}
<button onClick={() => updateFilter('minViews', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('minViews', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.minReviews && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Min Yorum: {parseInt(filters.minReviews).toLocaleString('tr-TR')}
<button onClick={() => updateFilter('minReviews', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('minReviews', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.maxReviews && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Max Yorum: {parseInt(filters.maxReviews).toLocaleString('tr-TR')}
<button onClick={() => updateFilter('maxReviews', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('maxReviews', '')} className="hover:text-orange-900">×</button>
</span>
)}
{filters.minRating && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
Min Puan: {parseFloat(filters.minRating).toFixed(1)}
<button onClick={() => updateFilter('minRating', '')} className="hover:text-blue-900">×</button>
<button onClick={() => updateFilter('minRating', '')} className="hover:text-orange-900">×</button>
</span>
)}
</div>
@@ -600,19 +599,19 @@ export default function ProductFinderTab({ allProducts }) {
{/* Product Grid */}
{filteredProducts.length === 0 ? (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
<div className="text-gray-400 mb-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-12 text-center">
<div className="text-slate-400 mb-4">
<svg className="mx-auto h-16 w-16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">Sonuç Bulunamadı</h3>
<p className="text-gray-500 mb-4">
<h3 className="text-xl font-semibold text-slate-900 mb-2">Sonuç Bulunamadı</h3>
<p className="text-slate-400 mb-4">
Aradığınız kriterlere uygun ürün bulunamadı. Lütfen filtrelerinizi değiştirin.
</p>
<button
onClick={clearFilters}
className="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors"
className="px-4 py-2 bg-orange-500 text-white rounded-xl hover:bg-orange-600 transition-colors"
>
Tüm Filtreleri Temizle
</button>
@@ -620,53 +619,53 @@ export default function ProductFinderTab({ allProducts }) {
) : (
<>
{/* Products Table */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
Görsel
</th>
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
Ürün Bilgisi
</th>
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
Marka
</th>
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
Kategori
</th>
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
Menşei
</th>
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
Fiyat
</th>
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
Satış
</th>
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
Görüntülenme
</th>
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
Yorum
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white divide-y divide-slate-200">
{paginatedProducts.map((product) => {
const productUrl = product.url?.startsWith('http') ? product.url : `https://www.trendyol.com${product.url}`
return (
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
<tr key={product.id} className="hover:bg-orange-50/30 transition-colors">
{/* Image */}
<td className="p-3 whitespace-nowrap">
<a
href={productUrl}
target="_blank"
rel="noopener noreferrer"
className="block w-16 h-16 bg-gray-100 rounded-xl overflow-hidden flex-shrink-0 hover:ring-2 hover:ring-orange-500 transition-all"
className="block w-16 h-16 bg-slate-100 rounded-xl overflow-hidden flex-shrink-0 hover:ring-2 hover:ring-orange-500 transition-all"
>
{product.image_url ? (
<img
@@ -676,7 +675,7 @@ export default function ProductFinderTab({ allProducts }) {
loading="lazy"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
<div className="w-full h-full flex items-center justify-center text-slate-400">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
@@ -692,34 +691,34 @@ export default function ProductFinderTab({ allProducts }) {
href={productUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-900 hover:text-orange-600 transition-colors mb-1 block"
className="text-sm font-medium text-slate-900 hover:text-orange-600 transition-colors mb-1 block"
title={product.name}
>
{product.name}
</a>
{product.barcode && (
<div className="text-xs text-gray-400 mt-1">Barkod: {product.barcode}</div>
<div className="text-xs text-slate-400 mt-1">Barkod: {product.barcode}</div>
)}
</div>
</td>
{/* Brand */}
<td className="p-3 whitespace-nowrap">
<div className="text-sm text-gray-900">{product.brand || '-'}</div>
<div className="text-sm text-slate-900">{product.brand || '-'}</div>
</td>
{/* Category */}
<td className="p-3 whitespace-nowrap">
<div className="text-sm text-gray-900">{product.category_name || '-'}</div>
<div className="text-sm text-slate-900">{product.category_name || '-'}</div>
</td>
{/* Country */}
<td className="p-3 whitespace-nowrap">
<div className="text-sm text-gray-900">
<div className="text-sm text-slate-900">
{product.country ? (
<span className="inline-flex items-center gap-1">
<span>{product.country}</span>
<span className="text-xs text-gray-500">({product.country_code})</span>
<span className="text-xs text-slate-400">({product.country_code})</span>
</span>
) : '-'}
</div>
@@ -727,28 +726,28 @@ export default function ProductFinderTab({ allProducts }) {
{/* Price */}
<td className="p-3 whitespace-nowrap text-right">
<div className="text-sm font-semibold text-blue-600">
<div className="text-sm font-semibold text-orange-500">
{product.price?.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</td>
{/* Orders */}
<td className="p-3 whitespace-nowrap text-right">
<div className="text-sm text-gray-900">
<div className="text-sm text-slate-900">
{(product.orders || 0).toLocaleString('tr-TR')}
</div>
</td>
{/* Views */}
<td className="p-3 whitespace-nowrap text-right">
<div className="text-sm text-gray-900">
<div className="text-sm text-slate-900">
{(product.page_views || 0).toLocaleString('tr-TR')}
</div>
</td>
{/* Reviews */}
<td className="p-3 whitespace-nowrap text-right">
<div className="text-sm text-gray-900">
<div className="text-sm text-slate-900">
{(product.reviews || product.rating_count || 0).toLocaleString('tr-TR')}
</div>
</td>
@@ -762,23 +761,23 @@ export default function ProductFinderTab({ allProducts }) {
{/* Pagination */}
{totalPages > 1 && (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600">
<div className="text-sm text-slate-500">
Sayfa {currentPage} / {totalPages} (Toplam {filteredProducts.length.toLocaleString('tr-TR')} ürün)
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
««
</button>
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Önceki
</button>
@@ -803,8 +802,8 @@ export default function ProductFinderTab({ allProducts }) {
onClick={() => setCurrentPage(pageNum)}
className={`px-3 py-1.5 text-sm border rounded-xl transition-colors ${
currentPage === pageNum
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 hover:bg-gray-50'
? 'bg-orange-500 text-white border-orange-500'
: 'border-slate-300 hover:bg-orange-50/30'
}`}
>
{pageNum}
@@ -816,14 +815,14 @@ export default function ProductFinderTab({ allProducts }) {
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Sonraki
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
»»
</button>

View File

@@ -0,0 +1,33 @@
// Reusable KPI card component - flat white design with colored icon
export default function KpiCard({ title, value, subtitle, icon: Icon, color = 'orange' }) {
const colorMap = {
orange: { bg: 'bg-orange-50', text: 'text-orange-500', border: 'border-orange-100' },
blue: { bg: 'bg-blue-50', text: 'text-blue-500', border: 'border-blue-100' },
emerald: { bg: 'bg-emerald-50', text: 'text-emerald-500', border: 'border-emerald-100' },
violet: { bg: 'bg-violet-50', text: 'text-violet-500', border: 'border-violet-100' },
rose: { bg: 'bg-rose-50', text: 'text-rose-500', border: 'border-rose-100' },
cyan: { bg: 'bg-cyan-50', text: 'text-cyan-500', border: 'border-cyan-100' },
amber: { bg: 'bg-amber-50', text: 'text-amber-500', border: 'border-amber-100' },
slate: { bg: 'bg-slate-50', text: 'text-slate-500', border: 'border-slate-100' },
}
const c = colorMap[color] || colorMap.orange
return (
<div className="bg-white rounded-xl p-5 border border-slate-200 hover:shadow-md transition-shadow duration-200">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-slate-500">{title}</span>
{Icon && (
<div className={`w-10 h-10 rounded-lg ${c.bg} ${c.border} border flex items-center justify-center`}>
<Icon size={20} className={c.text} />
</div>
)}
</div>
<div className="text-2xl font-bold text-slate-900">{value}</div>
{subtitle && (
<p className="text-xs text-slate-400 mt-1">{subtitle}</p>
)}
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { useState } from 'react'
import { useLocation } from 'react-router-dom'
import Sidebar from './Sidebar'
import TopBar from './TopBar'
export default function Layout({ children }) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const location = useLocation()
const isDashboard = location.pathname.startsWith('/reports/')
return (
<div className="min-h-screen bg-slate-50">
<Sidebar
collapsed={sidebarCollapsed}
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)}
/>
<TopBar
onMenuClick={() => setMobileOpen(true)}
sidebarCollapsed={sidebarCollapsed}
/>
{/* Main content */}
<main className={`
transition-all duration-300
${isDashboard
? ''
: sidebarCollapsed
? 'lg:pl-[72px]'
: 'lg:pl-64'
}
`}>
<div className={isDashboard ? '' : 'p-4 sm:p-6 lg:p-8'}>
{children}
</div>
</main>
</div>
)
}

View File

@@ -0,0 +1,113 @@
import { NavLink, useLocation } from 'react-router-dom'
import {
FileBarChart,
ListOrdered,
GitCompareArrows,
PanelLeftClose,
PanelLeft,
X
} from 'lucide-react'
const navItems = [
{ to: '/', icon: FileBarChart, label: 'Rapor Oluştur' },
{ to: '/reports', icon: ListOrdered, label: 'Raporlarım' },
{ to: '/compare', icon: GitCompareArrows, label: 'Karşılaştır' },
]
export default function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }) {
const location = useLocation()
const isDashboard = location.pathname.startsWith('/reports/')
// Hide sidebar on dashboard pages
if (isDashboard) return null
return (
<>
{/* Mobile overlay */}
{mobileOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={onMobileClose}
/>
)}
{/* Sidebar */}
<aside className={`
fixed top-0 left-0 h-full bg-slate-900 z-50
transition-all duration-300 ease-in-out
flex flex-col
${collapsed ? 'w-[72px]' : 'w-64'}
${mobileOpen ? 'translate-x-0' : '-translate-x-full'}
lg:translate-x-0
`}>
{/* Logo area */}
<div className={`flex items-center h-16 px-4 border-b border-slate-800 ${collapsed ? 'justify-center' : 'justify-between'}`}>
{!collapsed && (
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-orange-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">T</span>
</div>
<div>
<h1 className="text-white font-semibold text-sm leading-tight">Trendyol</h1>
<p className="text-slate-400 text-[11px] leading-tight">Analytics</p>
</div>
</div>
)}
{collapsed && (
<div className="w-8 h-8 bg-orange-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">T</span>
</div>
)}
{/* Mobile close */}
<button
onClick={onMobileClose}
className="lg:hidden text-slate-400 hover:text-white p-1"
>
<X size={20} />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 py-4 px-3 space-y-1">
{navItems.map((item) => {
const IconComp = item.icon
return (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
onClick={onMobileClose}
className={({ isActive }) => `
flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
transition-all duration-150
${isActive
? 'bg-orange-500/15 text-orange-400'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}
${collapsed ? 'justify-center' : ''}
`}
title={collapsed ? item.label : undefined}
>
<IconComp size={20} className="shrink-0" />
{!collapsed && <span>{item.label}</span>}
</NavLink>
)
})}
</nav>
{/* Collapse toggle - desktop only */}
<div className="hidden lg:block p-3 border-t border-slate-800">
<button
onClick={onToggle}
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors text-sm"
title={collapsed ? 'Menüyü genişlet' : 'Menüyü daralt'}
>
{collapsed ? <PanelLeft size={18} /> : <PanelLeftClose size={18} />}
{!collapsed && <span>Daralt</span>}
</button>
</div>
</aside>
</>
)
}

View File

@@ -0,0 +1,85 @@
// Reusable skeleton loading components
function SkeletonBox({ className = '' }) {
return <div className={`skeleton bg-slate-200 ${className}`} />
}
export function KpiSkeleton({ count = 4 }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="bg-white rounded-xl p-5 border border-slate-200">
<div className="flex items-center justify-between mb-3">
<SkeletonBox className="h-4 w-24" />
<SkeletonBox className="h-10 w-10 rounded-lg" />
</div>
<SkeletonBox className="h-8 w-20 mb-2" />
<SkeletonBox className="h-3 w-16" />
</div>
))}
</div>
)
}
export function TableSkeleton({ rows = 5, cols = 4 }) {
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Header */}
<div className="flex gap-4 p-4 border-b border-slate-100">
{Array.from({ length: cols }).map((_, i) => (
<SkeletonBox key={i} className="h-4 flex-1" />
))}
</div>
{/* Rows */}
{Array.from({ length: rows }).map((_, row) => (
<div key={row} className="flex gap-4 p-4 border-b border-slate-50">
{Array.from({ length: cols }).map((_, col) => (
<SkeletonBox key={col} className="h-4 flex-1" />
))}
</div>
))}
</div>
)
}
export function ChartSkeleton() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<SkeletonBox className="h-5 w-40 mb-6" />
<div className="flex items-end gap-2 h-48">
{[60, 80, 45, 90, 70, 55, 85, 65].map((h, i) => (
<SkeletonBox key={i} className="flex-1" style={{ height: `${h}%` }} />
))}
</div>
</div>
)
}
export function CardSkeleton({ count = 3 }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="bg-white rounded-xl p-5 border border-slate-200">
<SkeletonBox className="h-5 w-3/4 mb-3" />
<SkeletonBox className="h-4 w-1/2 mb-4" />
<SkeletonBox className="h-3 w-full mb-2" />
<SkeletonBox className="h-3 w-2/3" />
</div>
))}
</div>
)
}
// Full page loading skeleton
export function PageSkeleton() {
return (
<div className="space-y-6">
<KpiSkeleton />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ChartSkeleton />
<ChartSkeleton />
</div>
<TableSkeleton />
</div>
)
}

View File

@@ -0,0 +1,45 @@
import { useLocation } from 'react-router-dom'
import { Menu } from 'lucide-react'
const pageTitles = {
'/': { title: 'Rapor Oluştur', subtitle: 'Yeni kategori analiz raporu oluşturun' },
'/report': { title: 'Rapor Oluştur', subtitle: 'Yeni kategori analiz raporu oluşturun' },
'/reports': { title: 'Raporlarım', subtitle: 'Oluşturulan raporları görüntüleyin' },
}
export default function TopBar({ onMenuClick, sidebarCollapsed }) {
const location = useLocation()
const isDashboard = location.pathname.startsWith('/reports/')
// Hide topbar on dashboard pages (dashboard has its own header)
if (isDashboard) return null
const page = pageTitles[location.pathname] || { title: 'Trendyol Analytics', subtitle: '' }
return (
<header className={`
sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-200
transition-all duration-300
${sidebarCollapsed ? 'lg:pl-[72px]' : 'lg:pl-64'}
`}>
<div className="flex items-center justify-between h-16 px-4 sm:px-6">
<div className="flex items-center gap-3">
{/* Mobile hamburger */}
<button
onClick={onMenuClick}
className="lg:hidden p-2 -ml-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg"
>
<Menu size={20} />
</button>
<div>
<h2 className="text-lg font-semibold text-slate-900">{page.title}</h2>
{page.subtitle && (
<p className="text-xs text-slate-400 hidden sm:block">{page.subtitle}</p>
)}
</div>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,50 @@
// Trendyol Analytics - Chart Color Palette
// Consistent 8-color palette for all chart visualizations
export const CHART_COLORS = [
'#f97316', // orange-500 (primary)
'#3b82f6', // blue-500
'#10b981', // emerald-500
'#8b5cf6', // violet-500
'#f43f5e', // rose-500
'#06b6d4', // cyan-500
'#f59e0b', // amber-500
'#6366f1', // indigo-500
]
// Extended palette for larger datasets
export const CHART_COLORS_EXTENDED = [
...CHART_COLORS,
'#84cc16', // lime-500
'#ec4899', // pink-500
'#14b8a6', // teal-500
'#a855f7', // purple-500
]
// Semantic colors for specific use cases
export const STATUS_COLORS = {
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
}
// Styled tooltip for Recharts
export const CHART_TOOLTIP_STYLE = {
contentStyle: {
backgroundColor: '#1e293b', // slate-800
border: 'none',
borderRadius: '0.75rem',
padding: '12px 16px',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
},
labelStyle: {
color: '#f8fafc', // slate-50
fontWeight: 600,
marginBottom: '4px',
},
itemStyle: {
color: '#e2e8f0', // slate-200
fontSize: '13px',
},
}

View File

@@ -11,7 +11,9 @@ export const TAB_GROUPS = {
{ id: 'origin', name: 'Menşei' },
{ id: 'barcode', name: 'Barkod' },
{ id: 'keyword', name: 'Keyword Aracı' },
{ id: 'product-finder', name: 'Ürün Bulma' }
{ id: 'product-finder', name: 'Ürün Bulma' },
{ id: 'hidden-champions', name: 'Gizli Şampiyonlar' },
{ id: 'opportunity', name: 'Fırsat Haritası' }
]
}
}

View File

@@ -2,16 +2,73 @@
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #f8fafc; /* slate-50 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Hide scrollbar for Chrome, Safari and Opera */
/* Skeleton loading animation */
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.skeleton {
animation: skeleton-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 0.5rem;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1; /* slate-300 */
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8; /* slate-400 */
}
/* Hide scrollbar utility */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Page transition */
.page-enter {
opacity: 0;
transform: translateY(8px);
}
.page-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
/* Smooth transitions for interactive elements */
button, a, input, select, textarea {
transition: all 150ms ease;
}

View File

@@ -0,0 +1,114 @@
import * as XLSX from 'xlsx'
/**
* Export dashboard data to Excel
* @param {object} dashboardData - Full dashboard data
* @param {string} reportName - Report name for the file
*/
export function exportToExcel(dashboardData, reportName) {
if (!dashboardData) return
const wb = XLSX.utils.book_new()
const products = dashboardData.all_products || []
// Sheet 1: KPI Summary
const kpiData = []
const totalProducts = products.length
const totalOrders = products.reduce((s, p) => s + (p.orders || 0), 0)
const totalViews = products.reduce((s, p) => s + (p.page_views || 0), 0)
const avgPrice = totalProducts > 0
? Math.round(products.reduce((s, p) => s + (p.price || 0), 0) / totalProducts)
: 0
const totalRevenue = products.reduce((s, p) => s + ((p.price || 0) * (p.orders || 0)), 0)
// Unique brands
const uniqueBrands = new Set(products.map(p => p.brand).filter(Boolean))
kpiData.push(['Metrik', 'Değer'])
kpiData.push(['Rapor Adı', reportName])
kpiData.push(['Toplam Ürün', totalProducts])
kpiData.push(['Toplam Sipariş', totalOrders])
kpiData.push(['Toplam Görüntülenme', totalViews])
kpiData.push(['Ortalama Fiyat (₺)', avgPrice])
kpiData.push(['Toplam Ciro (₺)', Math.round(totalRevenue)])
kpiData.push(['Toplam Marka', uniqueBrands.size])
const kpiSheet = XLSX.utils.aoa_to_sheet(kpiData)
kpiSheet['!cols'] = [{ wch: 25 }, { wch: 20 }]
XLSX.utils.book_append_sheet(wb, kpiSheet, 'KPI Özet')
// Sheet 2: All Products
if (products.length > 0) {
const productRows = products.map(p => ({
'Ürün Adı': p.name || '',
'Marka': p.brand || '',
'Kategori': p.category_name || '',
'Fiyat (₺)': p.price || 0,
'Sipariş': p.orders || 0,
'Görüntülenme': p.page_views || 0,
'Rating': p.rating || 0,
'Yorum Sayısı': p.review_count || p.reviewCount || 0,
'Menşei': p.country || '',
'Barkod': p.barcode || '',
'URL': p.url || ''
}))
const productSheet = XLSX.utils.json_to_sheet(productRows)
productSheet['!cols'] = [
{ wch: 50 }, // name
{ wch: 20 }, // brand
{ wch: 25 }, // category
{ wch: 12 }, // price
{ wch: 10 }, // orders
{ wch: 15 }, // views
{ wch: 8 }, // rating
{ wch: 12 }, // reviews
{ wch: 15 }, // country
{ wch: 18 }, // barcode
{ wch: 40 }, // url
]
XLSX.utils.book_append_sheet(wb, productSheet, 'Tüm Ürünler')
}
// Sheet 3: Brand Summary
const brandMap = new Map()
products.forEach(p => {
const brand = p.brand || 'Bilinmeyen'
if (!brandMap.has(brand)) {
brandMap.set(brand, { name: brand, count: 0, orders: 0, revenue: 0 })
}
const b = brandMap.get(brand)
b.count++
b.orders += (p.orders || 0)
b.revenue += (p.price || 0) * (p.orders || 0)
})
const brandRows = Array.from(brandMap.values())
.sort((a, b) => b.orders - a.orders)
.map(b => ({
'Marka': b.name,
'Ürün Sayısı': b.count,
'Toplam Sipariş': b.orders,
'Toplam Ciro (₺)': Math.round(b.revenue)
}))
if (brandRows.length > 0) {
const brandSheet = XLSX.utils.json_to_sheet(brandRows)
brandSheet['!cols'] = [{ wch: 25 }, { wch: 12 }, { wch: 15 }, { wch: 18 }]
XLSX.utils.book_append_sheet(wb, brandSheet, 'Marka Özet')
}
// Generate filename
const date = new Date().toLocaleDateString('tr-TR').replace(/\./g, '-')
const safeName = (reportName || 'rapor').replace(/[^a-zA-Z0-9ğüşıöçĞÜŞİÖÇ\s-]/g, '').trim().replace(/\s+/g, '_')
const filename = `${safeName}_${date}.xlsx`
XLSX.writeFile(wb, filename)
}
/**
* Print the current report dashboard
*/
export function printReport() {
window.print()
}

34778
categories/Bebek_104158.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

32903
categories/Giyim_82.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
{
"report_name": "Mart Anne & Çocuk Raporua",
"category": "Anne & Çocuk",
"created_at": "2026-03-07T17:32:09.624752",
"total_subcategories": 1,
"total_products": 100,
"details": [
{
"category_id": 104158,
"category_name": "Bebek",
"success": true,
"total_products": 100,
"file_path": "../categories/Bebek_104158.json"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"report_name": "Mart Kadın Raporu",
"category": "Kadın",
"created_at": "2026-03-07T17:05:56.201611",
"total_subcategories": 1,
"total_products": 100,
"details": [
{
"category_id": 82,
"category_name": "Giyim",
"success": true,
"total_products": 100,
"file_path": "../categories/Giyim_82.json"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"report_name": "Mart Kozmetik Raporu",
"category": "Kozmetik",
"created_at": "2026-03-07T16:36:35.817598",
"total_subcategories": 1,
"total_products": 100,
"details": [
{
"category_id": 85,
"category_name": "Cilt Bakımı",
"success": true,
"total_products": 100,
"file_path": "../categories/Cilt_Bakımı_85.json"
}
]
}