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

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">
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<CategoryManagement />} />
<Route path="/report" element={<ReportGeneration />} />
<Route path="/reports" element={<ReportList />} />
<Route path="/reports/:reportId" element={<ReportDashboard />} />
</Routes>
</Suspense>
</main>
</div>
<Layout>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<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>
</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">
{category.children_count || 0} alt kategori
</p>
<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">
{subCat.name}
<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="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"
>
<ArrowLeft size={18} />
Geri
</button>
</div>
<button
onClick={() => navigate('/reports')}
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
Geri
</button>
</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 => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-6 py-4 font-medium whitespace-nowrap transition-colors ${
activeTab === tab.id
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
}`}
>
{tab.name}
</button>
))}
<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={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${
activeTab === tab.id
? '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>
) : (
<div className="space-y-2">
{/* Search Bar */}
<div className="relative">
<input
type="text"
placeholder="Alt kategori ara... (örn: Telefon)"
value={subCategorySearch}
onChange={(e) => setSubCategorySearch(e.target.value)}
{/* 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-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'
}`}
/>
{subCategorySearch && (
<button
onClick={() => setSubCategorySearch('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
×
</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`}
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>
) : (
<span className="text-slate-800 font-medium">{crumb.name}</span>
)}
</span>
</div>
<div className="flex gap-2">
{selectedSubCategories.length > 0 && (
<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'
}`}
>
Seçimi Temizle
</button>
)}
<button
onClick={() => {
if (!generating) {
const filteredSubs = subCategories.filter((subCat) =>
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
)
setSelectedSubCategories(filteredSubs)
}
}}
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'
}`}
>
{subCategorySearch ? 'Gösterilenleri Seç' : 'Tümünü Seç'}
</button>
</div>
))}
</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>
{/* 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)
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'
} ${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"
disabled={generating}
/>
<span className="text-sm font-medium text-gray-900">{subCat.name}</span>
</div>
</div>
</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>
)
})()}
{/* 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="Ara..."
value={subCategorySearch}
onChange={(e) => setSubCategorySearch(e.target.value)}
disabled={generating}
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 -translate-y-1/2 text-slate-300 hover:text-slate-500"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
)}
</div>
{/* 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="text-[11px] text-slate-400 hover:text-red-400 font-medium transition-colors ml-auto shrink-0"
>
Temizle
</button>
</div>
</div>
)}
{/* 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) 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])
}
}}
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' : ''}`}
>
<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>
{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}
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' : ''}`}
>
{/* Checkbox */}
<button
onClick={() => toggleSubCategory(subCat)}
disabled={generating}
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 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{' '}
{selectedSubCategories.length > 0 ? (
<>
<strong>{selectedSubCategories.length}</strong> seçili alt kategoriden veri çekilecek
</>
) : (
<>
<strong>{selectedCategory.children_count}</strong> alt kategoriden veri çekilecek
</>
)}
</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'
}`}
>
{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</>
)}
</button>
{/* Progress Bar */}
{generating && (
<div className="w-full max-w-md mt-6">
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-blue-600 h-3 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* 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 ? (
<span>{selectedSubCategories.length} seçili kategori</span>
) : (
<span>{selectedCategory.children_count || 0} alt kategori</span>
)}
</p>
<button
onClick={handleGenerateReport}
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"
>
Rapor Oluştur
</button>
</div>
)}
{/* Progress - shows during generation */}
{generating && (
<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-orange-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</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 className="bg-slate-900 rounded-xl overflow-hidden border border-slate-700">
{/* Terminal Header */}
<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-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-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>
<div className="bg-gray-900 rounded-lg p-4 max-h-96 overflow-y-auto border-2 border-gray-700">
{/* Terminal Header */}
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-700">
<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>
<span className="text-gray-400 text-xs font-mono ml-2">trendyol-analytics-terminal</span>
</div>
{/* Terminal Content */}
{/* 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,72 +594,66 @@ 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>
<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>
</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>
</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"
>
Raporu Görüntüle
</button>
<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-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="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-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>
)}

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">
Toplam {reports.length} rapor
</p>
<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) => (
<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"
>
{/* Report Header */}
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-semibold text-gray-900 text-base">
{report.name}
</h3>
<p className="text-sm text-gray-600 mt-1">
{report.category_name}
</p>
{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-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-slate-900 text-base">
{report.name}
</h3>
<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-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-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-slate-400 mb-4">
{formatDate(report.created_at)}
</div>
{/* Actions */}
<div className="flex flex-wrap gap-2">
<Link
to={`/reports/${report.id}`}
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="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"
>
<Trash2 size={14} />
</button>
</div>
</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">
{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">
{report.total_products.toLocaleString()}
</p>
</div>
</div>
{/* Date */}
<div className="text-xs text-gray-500 mb-4">
{formatDate(report.created_at)}
</div>
{/* Actions */}
<div className="flex space-x-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"
>
Görüntüle
</Link>
<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"
>
Sil
</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>
</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')}
</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>
</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">Toplam Görüntülenme</p>
<p className="text-3xl font-bold mt-2">
{overviewKPIs.totalViews.toLocaleString('tr-TR')}
</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="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>
</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>
</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')}
</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>
<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>
{/* 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="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>
{/* 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="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>
{/* 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>
)}
{/* 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 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()
}