feat: add 9 new dashboard features with export and comparison

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

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

5
.gitignore vendored
View File

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

View File

@@ -1,10 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="tr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -9,10 +9,12 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^1.13.2", "axios": "^1.13.2",
"lucide-react": "^0.577.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.9.5", "react-router-dom": "^7.9.5",
"recharts": "^3.4.1" "recharts": "^3.4.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.28.3", "@babel/cli": "^7.28.3",
@@ -2269,6 +2271,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "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": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2527,6 +2538,19 @@
], ],
"license": "CC-BY-4.0" "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": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2593,6 +2617,15 @@
"node": ">=6" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2658,6 +2691,18 @@
"node": ">=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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3329,6 +3374,15 @@
"node": ">= 6" "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": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -4069,6 +4123,15 @@
"yallist": "^3.0.2" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4744,6 +4807,18 @@
"node": ">=0.10.0" "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": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5004,6 +5079,24 @@
"node": ">= 8" "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": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5021,6 +5114,27 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -11,10 +11,12 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.13.2", "axios": "^1.13.2",
"lucide-react": "^0.577.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.9.5", "react-router-dom": "^7.9.5",
"recharts": "^3.4.1" "recharts": "^3.4.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.28.3", "@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 { 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 // Components are loaded only when user navigates to them
const CategoryManagement = lazy(() => import('./components/CategoryManagement'))
const ReportGeneration = lazy(() => import('./components/ReportGeneration')) const ReportGeneration = lazy(() => import('./components/ReportGeneration'))
const ReportList = lazy(() => import('./components/ReportList')) const ReportList = lazy(() => import('./components/ReportList'))
const ReportDashboard = lazy(() => import('./components/ReportDashboard')) const ReportDashboard = lazy(() => import('./components/ReportDashboard'))
const ReportComparison = lazy(() => import('./components/ReportComparison'))
// Loading component // Skeleton loading fallback
const LoadingFallback = () => ( const LoadingFallback = () => (
<div className="flex items-center justify-center min-h-[400px]"> <div className="page-enter page-enter-active">
<div className="text-center"> <PageSkeleton />
<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> </div>
) )
function App() { function App() {
return ( return (
<div className="min-h-screen bg-gray-50"> <Layout>
{/* Header */} <Suspense fallback={<LoadingFallback />}>
<header className="bg-white border-b border-gray-200 shadow-sm"> <Routes>
<div className="container mx-auto px-6 py-4"> <Route path="/" element={<ReportGeneration />} />
<h1 className="text-2xl font-bold text-gray-900">Trendyol Admin Panel</h1> <Route path="/report" element={<ReportGeneration />} />
<p className="text-sm text-gray-600 mt-1">Kategori Yönetimi & Veri Analizi</p> <Route path="/reports" element={<ReportList />} />
</div> <Route path="/reports/:reportId" element={<ReportDashboard />} />
</header> <Route path="/compare" element={<ReportComparison />} />
</Routes>
{/* Tabs - Only show on non-report pages */} </Suspense>
<Routes> </Layout>
<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>
) )
} }

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { API_URL, fetchWithTimeout } from '../config/api' 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() { function CategoryManagement() {
const [mainCategories, setMainCategories] = useState([]) const [mainCategories, setMainCategories] = useState([])
@@ -9,6 +9,8 @@ function CategoryManagement() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [loadingSubCategories, setLoadingSubCategories] = useState(false) const [loadingSubCategories, setLoadingSubCategories] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState(null)
// Breadcrumb trail for deep navigation
const [breadcrumb, setBreadcrumb] = useState([])
// Fetch main categories on mount // Fetch main categories on mount
useEffect(() => { useEffect(() => {
@@ -46,13 +48,39 @@ function CategoryManagement() {
const handleCategoryClick = (category) => { const handleCategoryClick = (category) => {
setSelectedCategory(category) setSelectedCategory(category)
setBreadcrumb([category])
fetchSubCategories(category.id) 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) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
@@ -68,94 +96,150 @@ function CategoryManagement() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl border border-slate-200 p-6">
<h1 className="text-2xl font-bold text-gray-900">Ana Kategoriler</h1> <h1 className="text-2xl font-bold text-slate-900">Kategori Yönetimi</h1>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-slate-500 mt-1">
Toplam {mainCategories.length} ana kategori Toplam {mainCategories.length} ana kategori
</p> </p>
</div> </div>
{/* Main Categories Grid */} {/* 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) => ( {mainCategories.map((category) => (
<div <div
key={category.id} key={category.id}
onClick={() => handleCategoryClick(category)} 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 ${ 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`}
selectedCategory?.id === category.id ? 'ring-2 ring-blue-200' : ''
}`}
> >
<div className="flex flex-col space-y-3"> <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">{category.name}</h3> <div>
<p className="text-sm text-gray-600"> <h3 className="text-base font-semibold text-slate-900">{category.name}</h3>
{category.children_count || 0} alt kategori <p className="text-sm text-slate-400 mt-1">
</p> {category.children_count || 0} alt kategori
</p>
</div>
<ChevronRight className="w-5 h-5 text-slate-300" />
</div> </div>
</div> </div>
))} ))}
</div> </div>
{/* Subcategories Table */} {/* Subcategories Panel */}
{selectedCategory && ( {selectedCategory && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200"> {/* Breadcrumb Navigation */}
<h2 className="text-xl font-semibold text-gray-900"> <div className="flex items-center gap-2 mb-4 pb-4 border-b border-slate-200">
{selectedCategory.name} - Alt Kategoriler <button
</h2> onClick={handleBackToMain}
<span className="text-sm text-gray-600"> 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 {subCategories.length} kategori
</span> </span>
</div> </div>
{loadingSubCategories ? ( {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 ? ( ) : 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ı. Bu kategoride alt kategori bulunamadı.
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-slate-200">
<thead className="bg-gray-50"> <thead className="bg-slate-50">
<tr> <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>
<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ı Kategori Adı
</th> </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 Trendyol ID
</th> </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 Trendyol URL
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-slate-200">
{subCategories.map((subCat, index) => ( {subCategories.map((subCat, index) => (
<tr key={subCat.id} className="hover:bg-gray-50"> <tr
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> 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} {index + 1}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">
{subCat.name} <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>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
{subCat.trendyol_category_id || '-'} {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>
<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 ? ( {subCat.trendyol_url ? (
<a <a
href={subCat.trendyol_url} href={subCat.trendyol_url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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 Trendyol'da
<ExternalLink className="w-3.5 h-3.5" />
</a> </a>
) : ( ) : (
<span className="text-gray-400">Link yok</span> <span className="text-slate-300"></span>
)} )}
</td> </td>
</tr> </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 { TAB_GROUPS, ALL_TABS } from '../constants/tabGroups'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../config/api' 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 { 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 BarcodeTab from './dashboard-tabs/BarcodeTab'
import OriginTab from './dashboard-tabs/OriginTab' import OriginTab from './dashboard-tabs/OriginTab'
import OverviewTab from './dashboard-tabs/OverviewTab' import OverviewTab from './dashboard-tabs/OverviewTab'
@@ -10,6 +12,9 @@ import BrandTab from './dashboard-tabs/BrandTab'
import CategoryTab from './dashboard-tabs/CategoryTab' import CategoryTab from './dashboard-tabs/CategoryTab'
import KeywordTab from './dashboard-tabs/KeywordTab' import KeywordTab from './dashboard-tabs/KeywordTab'
import ProductFinderTab from './dashboard-tabs/ProductFinderTab' 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() { function ReportDashboard() {
const { reportId } = useParams() const { reportId } = useParams()
@@ -1126,14 +1131,7 @@ function ReportDashboard() {
}, [dashboardData]) }, [dashboardData])
if (loading) { if (loading) {
return ( return <PageSkeleton />
<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>
)
} }
if (error) { if (error) {
@@ -1141,11 +1139,11 @@ function ReportDashboard() {
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<div className="text-red-500 text-6xl mb-4"></div> <div className="text-red-500 text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">Hata</h2> <h2 className="text-2xl font-bold text-slate-800 mb-2">Hata</h2>
<p className="text-gray-600 mb-4">{error}</p> <p className="text-slate-500 mb-4">{error}</p>
<button <button
onClick={() => navigate('/reports')} 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 Raporlara Dön
</button> </button>
@@ -1155,40 +1153,74 @@ function ReportDashboard() {
} }
return ( 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"> <div className="w-full px-4 py-6">
{/* Header */} {/* 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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">{dashboardData.report_name}</h1> <h1 className="text-3xl font-bold text-slate-900">{dashboardData.report_name}</h1>
<p className="text-gray-600 mt-1">{dashboardData.category_name}</p> <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> </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>
</div> </div>
{/* Tab Navigation */} {/* Tab Navigation */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 mb-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 mb-6 p-3">
<div className="flex border-b border-gray-200 overflow-x-auto"> <div className="flex gap-2 overflow-x-auto">
{ALL_TABS.map(tab => ( {ALL_TABS.map(tab => {
<button const TAB_ICONS = {
key={tab.id} 'overview': BarChart3,
onClick={() => setActiveTab(tab.id)} 'brand': Award,
className={`px-6 py-4 font-medium whitespace-nowrap transition-colors ${ 'category': Grid3X3,
activeTab === tab.id 'origin': Globe,
? 'border-b-2 border-blue-500 text-blue-600' 'barcode': Barcode,
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50' 'keyword': Key,
}`} 'product-finder': Search,
> 'hidden-champions': Trophy,
{tab.name} 'opportunity': Target
</button> }
))} 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>
</div> </div>
@@ -1202,6 +1234,8 @@ function ReportDashboard() {
topSellingBrands={topSellingBrands} topSellingBrands={topSellingBrands}
topSellingCategories={topSellingCategories} topSellingCategories={topSellingCategories}
mostViewedCategories={mostViewedCategories} mostViewedCategories={mostViewedCategories}
reportId={reportId}
allProducts={dashboardData?.all_products || []}
/> />
)} )}
@@ -1240,6 +1274,16 @@ function ReportDashboard() {
{activeTab === 'product-finder' && ( {activeTab === 'product-finder' && (
<ProductFinderTab allProducts={dashboardData.all_products || []} /> <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> </div>
</div> </div>

View File

@@ -1,11 +1,12 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG, calculateNextDelay } from '../config/api' import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG, calculateNextDelay } from '../config/api'
import { Search, Loader2, ChevronRight, ArrowLeft, X, Check } from 'lucide-react'
function ReportGeneration() { function ReportGeneration() {
const [mainCategories, setMainCategories] = useState([]) const [mainCategories, setMainCategories] = useState([])
const [selectedCategory, setSelectedCategory] = useState(null) const [selectedCategory, setSelectedCategory] = useState(null)
const [subCategories, setSubCategories] = useState([]) const [subCategories, setSubCategories] = useState([])
const [selectedSubCategories, setSelectedSubCategories] = useState([]) // Changed to array for multi-select const [selectedSubCategories, setSelectedSubCategories] = useState([])
const [loadingSubCategories, setLoadingSubCategories] = useState(false) const [loadingSubCategories, setLoadingSubCategories] = useState(false)
const [subCategorySearch, setSubCategorySearch] = useState('') const [subCategorySearch, setSubCategorySearch] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -18,6 +19,8 @@ function ReportGeneration() {
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [showCompletionModal, setShowCompletionModal] = useState(false) const [showCompletionModal, setShowCompletionModal] = useState(false)
const [completionData, setCompletionData] = useState(null) const [completionData, setCompletionData] = useState(null)
const [subBreadcrumb, setSubBreadcrumb] = useState([])
const [showSearch, setShowSearch] = useState(false)
const pollTimeoutRef = useRef(null) const pollTimeoutRef = useRef(null)
const isMountedRef = useRef(true) const isMountedRef = useRef(true)
const logsEndRef = useRef(null) const logsEndRef = useRef(null)
@@ -34,7 +37,6 @@ function ReportGeneration() {
} }
}, []) }, [])
// Auto-scroll to bottom of logs
useEffect(() => { useEffect(() => {
if (logsEndRef.current) { if (logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' }) logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
@@ -62,11 +64,15 @@ function ReportGeneration() {
} }
} }
const fetchSubCategories = async (categoryId) => { const fetchSubCategories = async (categoryId, resetSelection = true) => {
setLoadingSubCategories(true) setLoadingSubCategories(true)
setSubCategories([]) setSubCategories([])
setSelectedSubCategories([]) // Clear selected subcategories if (resetSelection) {
setSubCategorySearch('') // Clear search when loading new category setSelectedSubCategories([])
setSubBreadcrumb([])
}
setSubCategorySearch('')
setShowSearch(false)
try { try {
const response = await fetchWithTimeout(`${API_URL}/categories/${categoryId}/children`) const response = await fetchWithTimeout(`${API_URL}/categories/${categoryId}/children`)
if (!response.ok) throw new Error('Failed to fetch sub-categories') 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 addLog = (message, type = 'info') => {
const timestamp = new Date().toLocaleTimeString('tr-TR') const timestamp = new Date().toLocaleTimeString('tr-TR')
setLogs((prev) => [...prev, { timestamp, message, type }]) setLogs((prev) => [...prev, { timestamp, message, type }])
@@ -95,8 +127,6 @@ function ReportGeneration() {
alert('Lütfen önce bir kategori seçin!') alert('Lütfen önce bir kategori seçin!')
return return
} }
// Show modal first to get report name
setReportName(`${new Date().toLocaleDateString('tr-TR', { month: 'long' })} ${selectedCategory.name} Raporu`) setReportName(`${new Date().toLocaleDateString('tr-TR', { month: 'long' })} ${selectedCategory.name} Raporu`)
setShowNameModal(true) setShowNameModal(true)
} }
@@ -113,13 +143,11 @@ function ReportGeneration() {
setLogs([]) setLogs([])
try { try {
// Prepare request body
const requestBody = { const requestBody = {
name: reportName, name: reportName,
category_id: selectedCategory.id category_id: selectedCategory.id
} }
// Add subcategory_ids if subcategories are selected
if (selectedSubCategories.length > 0) { if (selectedSubCategories.length > 0) {
requestBody.subcategory_ids = selectedSubCategories.map(cat => cat.id) requestBody.subcategory_ids = selectedSubCategories.map(cat => cat.id)
} }
@@ -129,10 +157,6 @@ function ReportGeneration() {
console.log(' - Kategori ID:', requestBody.category_id) console.log(' - Kategori ID:', requestBody.category_id)
console.log(' - Alt kategori IDs:', requestBody.subcategory_ids) 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({ const params = new URLSearchParams({
name: requestBody.name, name: requestBody.name,
category_id: requestBody.category_id, category_id: requestBody.category_id,
@@ -142,7 +166,6 @@ function ReportGeneration() {
const sseUrl = `${API_URL}/api/reports/create?${params}` const sseUrl = `${API_URL}/api/reports/create?${params}`
console.log('🌐 SSE URL:', sseUrl) console.log('🌐 SSE URL:', sseUrl)
// Start SSE connection
const eventSource = new EventSource(sseUrl) const eventSource = new EventSource(sseUrl)
console.log('📡 EventSource oluşturuldu') console.log('📡 EventSource oluşturuldu')
@@ -155,12 +178,10 @@ function ReportGeneration() {
try { try {
const data = JSON.parse(event.data) const data = JSON.parse(event.data)
// Update progress
if (data.progress !== undefined) { if (data.progress !== undefined) {
setProgress(data.progress) setProgress(data.progress)
} }
// Add log message
if (data.message) { if (data.message) {
const timestamp = new Date().toLocaleTimeString('tr-TR') const timestamp = new Date().toLocaleTimeString('tr-TR')
setLogs(prev => [...prev, { setLogs(prev => [...prev, {
@@ -170,23 +191,17 @@ function ReportGeneration() {
}]) }])
} }
// Handle completion
if (data.type === 'complete') { if (data.type === 'complete') {
eventSource.close() eventSource.close()
// Store completion data
setCompletionData({ setCompletionData({
report_id: data.report_id, report_id: data.report_id,
total_products: data.total_products, total_products: data.total_products,
successful: data.successful successful: data.successful
}) })
// Show completion modal
setShowCompletionModal(true) setShowCompletionModal(true)
setGenerating(false) setGenerating(false)
} }
// Handle error
if (data.type === 'error' && data.progress === -1) { if (data.type === 'error' && data.progress === -1) {
eventSource.close() eventSource.close()
setGenerating(false) setGenerating(false)
@@ -199,8 +214,6 @@ function ReportGeneration() {
eventSource.onerror = (error) => { eventSource.onerror = (error) => {
console.error('❌ SSE Error:', error) console.error('❌ SSE Error:', error)
console.error('EventSource readyState:', eventSource.readyState)
console.error('EventSource url:', eventSource.url)
eventSource.close() eventSource.close()
addLog('Bağlantı hatası oluştu', 'error') addLog('Bağlantı hatası oluştu', 'error')
setGenerating(false) setGenerating(false)
@@ -214,7 +227,6 @@ function ReportGeneration() {
const handleViewReport = () => { const handleViewReport = () => {
if (completionData && completionData.report_id) { if (completionData && completionData.report_id) {
// Navigate to report dashboard
window.location.href = `/reports/${completionData.report_id}` window.location.href = `/reports/${completionData.report_id}`
} }
} }
@@ -222,7 +234,10 @@ function ReportGeneration() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
@@ -235,35 +250,36 @@ function ReportGeneration() {
) )
} }
return ( const filteredSubCategories = subCategories.filter((subCat) =>
<div className="space-y-6"> subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
{/* 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>
{/* Category Selection */} return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="space-y-5">
<h2 className="text-lg font-semibold text-gray-900 mb-4"> {/* Main Category Selection - Horizontal Pills */}
1. Kategori Seçin <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
</h2> <div className="flex items-center justify-between mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-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) => ( {mainCategories.map((category) => (
<div <button
key={category.id} key={category.id}
onClick={() => { onClick={() => {
if (!generating) { if (!generating) {
@@ -271,263 +287,290 @@ function ReportGeneration() {
fetchSubCategories(category.id) 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 ${ disabled={generating}
selectedCategory?.id === category.id ? 'ring-2 ring-blue-200' : '' 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' : ''}`} } ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
<div className="flex flex-col space-y-2"> {category.name}
<h3 className="text-sm font-semibold text-gray-900">{category.name}</h3> </button>
<p className="text-xs text-gray-600">
{category.children_count || 0} alt kategori
</p>
</div>
</div>
))} ))}
</div> </div>
</div> </div>
{/* Sub-Category Selection (Optional) */} {/* Sub-Category Selection - Clean List */}
{selectedCategory && subCategories.length > 0 && ( {selectedCategory && (subCategories.length > 0 || loadingSubCategories) && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<h2 className="text-lg font-semibold text-gray-900 mb-4"> {/* Header */}
2. Alt Kategori Seçin (Opsiyonel) <div className="px-5 py-4 border-b border-slate-100">
</h2> <div className="flex items-center justify-between">
<div className="space-y-4"> <div className="flex items-center gap-2.5">
<p className="text-sm text-gray-600"> {subBreadcrumb.length > 0 && (
İsterseniz sadece bir alt kategori için rapor oluşturabilirsiniz. Seçmezseniz tüm alt kategoriler için rapor oluşturulur. <button
</p> onClick={() => !generating && handleBreadcrumbBack(subBreadcrumb.length >= 2 ? subBreadcrumb.length - 2 : -1)}
{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)}
disabled={generating} 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 ${ 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"
generating ? 'opacity-50 cursor-not-allowed bg-gray-100' : 'bg-white' >
}`} <ArrowLeft className="w-3.5 h-3.5" />
/> </button>
{subCategorySearch && ( )}
<button <div className="flex items-center gap-1.5 text-sm">
onClick={() => setSubCategorySearch('')} <button
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" 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}
</button> >
)} {selectedCategory.name}
</div> </button>
{subBreadcrumb.map((crumb, index) => (
{/* Selection info and controls */} <span key={crumb.id} className="flex items-center gap-1.5">
<div className="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200"> <ChevronRight className="w-3 h-3 text-slate-300" />
<div className="flex items-center gap-2"> {index < subBreadcrumb.length - 1 ? (
<span className="font-medium text-gray-700"> <button
{selectedSubCategories.length === 0 onClick={() => !generating && handleBreadcrumbBack(index)}
? 'Tüm Alt Kategoriler Seçili' className="text-orange-500 hover:text-orange-600 font-medium"
: `${selectedSubCategories.length} Kategori Seçildi`} disabled={generating}
>
{crumb.name}
</button>
) : (
<span className="text-slate-800 font-medium">{crumb.name}</span>
)}
</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>
<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 */} {/* Search - collapsible */}
{(() => { {showSearch && (
const filteredSubCategories = subCategories.filter((subCat) => <div className="relative mt-3">
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase()) <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-300" />
) <input
type="text"
return filteredSubCategories.length > 0 ? ( placeholder="Ara..."
<div className="max-h-64 overflow-y-auto space-y-2 border rounded-lg p-2"> value={subCategorySearch}
{filteredSubCategories.map((subCat) => { onChange={(e) => setSubCategorySearch(e.target.value)}
const isSelected = selectedSubCategories.some(cat => cat.id === subCat.id) disabled={generating}
return ( autoFocus
<div 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"
key={subCat.id} />
onClick={() => { {subCategorySearch && (
if (!generating) { <button
if (isSelected) { onClick={() => setSubCategorySearch('')}
// Remove from selection className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-300 hover:text-slate-500"
setSelectedSubCategories(prev => >
prev.filter(cat => cat.id !== subCat.id) <X className="w-3.5 h-3.5" />
) </button>
} 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>
)
})()}
</div> </div>
)} )}
</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> </div>
)} )}
{/* Generate Button */} {/* Generate Button - compact inline */}
{selectedCategory && ( {selectedCategory && !generating && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="flex items-center justify-between bg-white rounded-xl shadow-sm border border-slate-200 px-5 py-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4"> <p className="text-sm text-slate-500">
{subCategories.length > 0 ? '3. Rapor Oluştur' : '2. Rapor Oluştur'} <span className="font-semibold text-slate-800">{selectedCategory.name}</span>
</h2> <span className="mx-1.5 text-slate-300">·</span>
<div className="flex flex-col items-center justify-center py-4"> {selectedSubCategories.length > 0 ? (
<p className="text-gray-600 mb-4 text-center"> <span>{selectedSubCategories.length} seçili kategori</span>
<strong>{selectedCategory.name}</strong> kategorisi için{' '} ) : (
{selectedSubCategories.length > 0 ? ( <span>{selectedCategory.children_count || 0} alt kategori</span>
<>
<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>
)} )}
</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>
</div> </div>
)} )}
{/* Terminal Logs */} {/* Terminal Logs */}
{logs.length > 0 && ( {logs.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-slate-900 rounded-xl overflow-hidden border border-slate-700">
<div className="flex items-center justify-between mb-4"> {/* Terminal Header */}
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2"> <div className="flex items-center gap-2 px-4 py-2.5 border-b border-slate-700/50">
<span>Terminal - Rapor Oluşturuluyor</span> <div className="flex gap-1.5">
</h3> <div className="w-2.5 h-2.5 rounded-full bg-red-500/80"></div>
<div className="flex items-center gap-2"> <div className="w-2.5 h-2.5 rounded-full bg-yellow-500/80"></div>
<div className="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div> <div className="w-2.5 h-2.5 rounded-full bg-green-500/80"></div>
<span className="text-sm text-gray-600">Canlı</span>
</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>
<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) => ( {logs.map((log, index) => (
<div <div
key={index} key={index}
className={`font-mono text-sm mb-2 ${ className={`font-mono text-xs mb-1.5 ${
log.type === 'error' log.type === 'error'
? 'text-red-400' ? 'text-red-400'
: log.type === 'success' : log.type === 'success'
@@ -538,14 +581,12 @@ function ReportGeneration() {
? 'text-blue-400' ? 'text-blue-400'
: log.type === 'processing' : log.type === 'processing'
? 'text-cyan-400' ? '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> </div>
))} ))}
{/* Auto-scroll anchor */}
<div ref={logsEndRef} /> <div ref={logsEndRef} />
</div> </div>
</div> </div>
@@ -553,72 +594,66 @@ function ReportGeneration() {
{/* Name Modal */} {/* Name Modal */}
{showNameModal && ( {showNameModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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-lg p-8 max-w-md w-full mx-4"> <div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Rapora Ad Verin</h2> <h2 className="text-lg font-bold text-slate-900 mb-1">Rapor Adı</h2>
<p className="text-gray-600 mb-6"> <p className="text-sm text-slate-400 mb-4">Raporunuz için bir isim belirleyin</p>
Rapor başarıyla oluşturuldu! Kaydetmek için bir isim verin.
</p>
<input <input
type="text" type="text"
value={reportName} value={reportName}
onChange={(e) => setReportName(e.target.value)} 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" 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 Ayı Kozmetik Raporu" placeholder="Örn: Kasım Kozmetik Raporu"
autoFocus autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleStartReportGeneration()}
/> />
<div className="flex space-x-4"> <div className="flex gap-3">
<button <button
onClick={() => { onClick={() => {
setShowNameModal(false) setShowNameModal(false)
setGenerating(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 İptal
</button> </button>
<button <button
onClick={handleStartReportGeneration} 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> </button>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Completion Modal - Shows when report is 100% complete */} {/* Completion Modal */}
{showCompletionModal && completionData && ( {showCompletionModal && completionData && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <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-xl p-8 max-w-md w-full shadow-2xl"> <div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl text-center">
<div className="text-center"> <div className="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-20 h-20 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} />
<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> </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>
</div> </div>
)} )}

View File

@@ -1,11 +1,14 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { API_URL, fetchWithTimeout } from '../config/api' 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() { function ReportList() {
const navigate = useNavigate()
const [reports, setReports] = useState([]) const [reports, setReports] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [enrichingReports, setEnrichingReports] = useState({}) // { reportId: { status, progress, enriched } }
useEffect(() => { useEffect(() => {
fetchReports() fetchReports()
@@ -19,6 +22,16 @@ function ReportList() {
if (!response.ok) throw new Error('Failed to fetch reports') if (!response.ok) throw new Error('Failed to fetch reports')
const data = await response.json() const data = await response.json()
setReports(data) 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) { } catch (err) {
setError(err.message) setError(err.message)
} finally { } 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 formatDate = (dateString) => {
const date = new Date(dateString) const date = new Date(dateString)
return date.toLocaleDateString('tr-TR', { return date.toLocaleDateString('tr-TR', {
@@ -60,14 +141,14 @@ function ReportList() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
if (error) { if (error) {
return ( 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> <p className="text-red-700">Hata: {error}</p>
</div> </div>
) )
@@ -76,8 +157,8 @@ function ReportList() {
if (reports.length === 0) { if (reports.length === 0) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<h3 className="text-xl font-semibold text-gray-800 mb-2">Henüz Rapor Yok</h3> <h3 className="text-xl font-semibold text-slate-800 mb-2">Henüz Rapor Yok</h3>
<p className="text-gray-600 mb-6"> <p className="text-slate-500 mb-6">
"Rapor Oluştur" sekmesinden ilk raporunuzu oluşturun "Rapor Oluştur" sekmesinden ilk raporunuzu oluşturun
</p> </p>
</div> </div>
@@ -87,70 +168,123 @@ function ReportList() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h1 className="text-2xl font-bold text-gray-900">Raporlarım</h1> <div className="flex items-center justify-between">
<p className="text-sm text-gray-600 mt-1"> <div>
Toplam {reports.length} rapor <h1 className="text-2xl font-bold text-slate-900">Raporlarım</h1>
</p> <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> </div>
{/* Reports Grid */} {/* Reports Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{reports.map((report) => ( {reports.map((report) => {
<div const enrichState = enrichingReports[report.id]
key={report.id} const isEnriched = enrichState?.enriched || report.is_enriched
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" const isEnriching = enrichState?.status === 'loading'
> const enrichProgress = enrichState?.progress || 0
{/* Report Header */}
<div className="flex items-start justify-between mb-4"> return (
<div> <div
<h3 className="font-semibold text-gray-900 text-base"> key={report.id}
{report.name} 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"
</h3> >
<p className="text-sm text-gray-600 mt-1"> {/* Report Header */}
{report.category_name} <div className="flex items-start justify-between mb-4">
</p> <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>
</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>
</div> </div>
) )

View File

@@ -1,5 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ScatterChart, Scatter } from 'recharts' 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 }) { export default function BarcodeTab({ barcodeAnalytics }) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: null }) const [sortConfig, setSortConfig] = useState({ key: null, direction: null })
@@ -60,7 +63,7 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (!barcodeAnalytics) { if (!barcodeAnalytics) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
@@ -70,100 +73,58 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Row 1: KPI Cards */} {/* Row 1: KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Total Products with Barcode */} {/* Total Products with Barcode */}
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white"> <KpiCard
<div className="flex items-center justify-between"> title="Barkodlu Ürün"
<div> value={barcodeAnalytics.kpis.totalWithBarcode.toLocaleString('tr-TR')}
<p className="text-blue-100 text-sm font-medium">Barkodlu Ürün</p> icon={Barcode}
<p className="text-3xl font-bold mt-2"> color="blue"
{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>
{/* Total Countries */} {/* Total Countries */}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white"> <KpiCard
<div className="flex items-center justify-between"> title="Tespit Edilen Ülke"
<div> value={barcodeAnalytics.kpis.totalCountries}
<p className="text-green-100 text-sm font-medium">Tespit Edilen Ülke</p> subtitle={barcodeAnalytics.kpis.undetectedProducts > 0 ? `${barcodeAnalytics.kpis.undetectedProducts.toLocaleString('tr-TR')} tespit edilemedi` : undefined}
<p className="text-3xl font-bold mt-2"> icon={Globe}
{barcodeAnalytics.kpis.totalCountries} color="emerald"
</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>
{/* Domestic Share */} {/* Domestic Share */}
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white"> <KpiCard
<div className="flex items-center justify-between"> title="Yerli Ürün Payı"
<div> value={`${barcodeAnalytics.kpis.domesticShare}%`}
<p className="text-purple-100 text-sm font-medium">Yerli Ürün Payı</p> subtitle="Türkiye menşeili"
<p className="text-3xl font-bold mt-2"> icon={Flag}
{barcodeAnalytics.kpis.domesticShare}% color="violet"
</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>
{/* Top Country */} {/* Top Country */}
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white"> <KpiCard
<div className="flex items-center justify-between"> title="En Çok Ürün"
<div> value={barcodeAnalytics.kpis.topCountry}
<p className="text-orange-100 text-sm font-medium">En Çok Ürün</p> subtitle={`${barcodeAnalytics.kpis.topCountryShare.toFixed(1)}% pay`}
<p className="text-2xl font-bold mt-2"> icon={Award}
{barcodeAnalytics.kpis.topCountry} color="orange"
</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>
</div> </div>
{/* Row 2: Category-Country Heatmap - FULL WIDTH */} {/* Row 2: Category-Country Heatmap - FULL WIDTH */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kategori-Ülke Isı Haritası (Top 10x10)</h3> <h3 className="text-lg font-semibold text-slate-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> <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 ? ( {barcodeAnalytics.categoryCountryMatrix && barcodeAnalytics.categoryCountryMatrix.length > 0 ? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full border-collapse"> <table className="min-w-full border-collapse">
<thead> <thead>
<tr> <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 Kategori / Ülke
</th> </th>
{barcodeAnalytics.topCountriesForHeatmap.map((country) => ( {barcodeAnalytics.topCountriesForHeatmap.map((country) => (
<th <th
key={country.name} 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' }} style={{ minWidth: '80px' }}
> >
{country.name} {country.name}
@@ -174,7 +135,7 @@ export default function BarcodeTab({ barcodeAnalytics }) {
<tbody> <tbody>
{barcodeAnalytics.topCategories.map((category) => ( {barcodeAnalytics.topCategories.map((category) => (
<tr key={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} {category}
</td> </td>
{barcodeAnalytics.topCountriesForHeatmap.map((country) => { {barcodeAnalytics.topCountriesForHeatmap.map((country) => {
@@ -184,8 +145,8 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (!cell) { if (!cell) {
return ( return (
<td key={country.name} className="border border-gray-200 px-2 py-2 bg-gray-50"> <td key={country.name} className="border border-slate-200 px-2 py-2 bg-slate-50">
<div className="text-center text-xs text-gray-400">-</div> <div className="text-center text-xs text-slate-400">-</div>
</td> </td>
) )
} }
@@ -197,12 +158,12 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (intensity > 0.7) return 'bg-blue-600 text-white' if (intensity > 0.7) return 'bg-blue-600 text-white'
if (intensity > 0.5) return 'bg-blue-500 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.3) return 'bg-blue-400 text-white'
if (intensity > 0.15) return 'bg-blue-300 text-gray-900' if (intensity > 0.15) return 'bg-blue-300 text-slate-900'
return 'bg-blue-100 text-gray-900' return 'bg-blue-100 text-slate-900'
} }
return ( return (
<td key={country.name} className="border border-gray-200 p-0"> <td key={country.name} className="border border-slate-200 p-0">
<div <div
className={`px-2 py-2 ${getColor(intensity)} text-center cursor-help transition-all hover:scale-105`} 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`} 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 */} {/* Legend */}
<div className="mt-4 flex items-center justify-end gap-2 text-xs"> <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="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-100 border border-slate-300"></div>
<div className="w-6 h-4 bg-blue-300 border border-gray-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-gray-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-gray-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-gray-300"></div> <div className="w-6 h-4 bg-blue-600 border border-slate-300"></div>
</div> </div>
<span className="text-gray-600">Yüksek</span> <span className="text-slate-500">Yüksek</span>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center h-64 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300"> <div className="flex items-center justify-center h-64 bg-slate-50 rounded-xl border-2 border-dashed border-slate-300">
<p className="text-gray-500 text-sm">Veri yükleniyor...</p> <p className="text-slate-400 text-sm">Veri yükleniyor...</p>
</div> </div>
)} )}
</div> </div>
{/* Row 3: Top 20 Countries - FULL WIDTH with 10x10 Grid */} {/* Row 3: Top 20 Countries - FULL WIDTH with 10x10 Grid */}
<div className="bg-white rounded-lg shadow-sm p-6"> <div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4"> <h3 className="text-lg font-semibold text-slate-800 mb-4">
En Çok Satan Ülkeler (Top 20) En Çok Satan Ülkeler (Top 20)
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* Left Column: Countries 1-10 */} {/* Left Column: Countries 1-10 */}
<div className="space-y-2"> <div className="space-y-2">
{barcodeAnalytics.topByOrders.slice(0, 10).map((country, index) => ( {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"> <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} {index + 1}
</span> </span>
<span className="font-semibold text-gray-800">{country.name}</span> <span className="font-semibold text-slate-800">{country.name}</span>
</div> </div>
<div className="text-right space-y-1"> <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')} <span className="font-semibold">Satış:</span> {country.totalOrders.toLocaleString('tr-TR')}
</p> </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')} <span className="font-semibold">Ciro:</span> {country.totalRevenue.toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -270,18 +231,18 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Right Column: Countries 11-20 */} {/* Right Column: Countries 11-20 */}
<div className="space-y-2"> <div className="space-y-2">
{barcodeAnalytics.topByOrders.slice(10, 20).map((country, index) => ( {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"> <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"> <span className="flex items-center justify-center w-8 h-8 bg-purple-500 text-white rounded-full font-bold text-sm">
{index + 11} {index + 11}
</span> </span>
<span className="font-semibold text-gray-800">{country.name}</span> <span className="font-semibold text-slate-800">{country.name}</span>
</div> </div>
<div className="text-right space-y-1"> <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')} <span className="font-semibold">Satış:</span> {country.totalOrders.toLocaleString('tr-TR')}
</p> </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')} <span className="font-semibold">Ciro:</span> {country.totalRevenue.toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -294,8 +255,8 @@ export default function BarcodeTab({ barcodeAnalytics }) {
{/* Row 4: Charts - 50/50 Split */} {/* Row 4: Charts - 50/50 Split */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Country Sales Bar Chart (Top 15) */} {/* Country Sales Bar Chart (Top 15) */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ülke Bazlı Satış (Top 15)</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Ülke Bazlı Satış (Top 15)</h3>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<BarChart data={barcodeAnalytics.topByOrders.slice(0, 15)}> <BarChart data={barcodeAnalytics.topByOrders.slice(0, 15)}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" /> <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
@@ -309,16 +270,16 @@ export default function BarcodeTab({ barcodeAnalytics }) {
<YAxis tick={{ fontSize: 12 }} /> <YAxis tick={{ fontSize: 12 }} />
<Tooltip <Tooltip
formatter={(value) => value.toLocaleString('tr-TR')} 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> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
{/* Price vs Revenue Scatter Chart */} {/* Price vs Revenue Scatter Chart */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ortalama Fiyat / Ciro İlişkisi</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Ortalama Fiyat / Ciro İlişkisi</h3>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<ScatterChart> <ScatterChart>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" /> <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
@@ -341,12 +302,12 @@ export default function BarcodeTab({ barcodeAnalytics }) {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload const data = payload[0].payload
return ( return (
<div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3"> <div className="bg-slate-800 border-none rounded-xl shadow-lg p-3">
<p className="font-semibold text-gray-900 mb-2 text-sm">{data.name}</p> <p className="font-semibold text-slate-50 mb-2 text-sm">{data.name}</p>
<p className="font-semibold text-gray-900 text-sm"> <p className="font-semibold text-slate-200 text-sm">
Ciro: {data.totalRevenue.toLocaleString('tr-TR')} Ciro: {data.totalRevenue.toLocaleString('tr-TR')}
</p> </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')} Satış: {data.totalOrders.toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -357,91 +318,91 @@ export default function BarcodeTab({ barcodeAnalytics }) {
/> />
<Scatter <Scatter
data={barcodeAnalytics.countries.filter(c => c.totalRevenue > 0)} data={barcodeAnalytics.countries.filter(c => c.totalRevenue > 0)}
fill="#8b5cf6" fill={CHART_COLORS[3]}
name="Ülkeler" name="Ülkeler"
/> />
</ScatterChart> </ScatterChart>
</ResponsiveContainer> </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) Her nokta bir ülkeyi temsil eder (Ortalama fiyat vs Toplam ciro)
</p> </p>
</div> </div>
</div> </div>
{/* Row 5: Detailed Country Comparison Table */} {/* Row 5: Detailed Country Comparison Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Ülke Karşılaştırması</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Ülke Karşılaştırması</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <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> <tr>
<th <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')} onClick={() => handleSort('name')}
> >
Ülke{renderSortIndicator('name')} Ülke{renderSortIndicator('name')}
</th> </th>
<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')} onClick={() => handleSort('productCount')}
> >
Ürün{renderSortIndicator('productCount')} Ürün{renderSortIndicator('productCount')}
</th> </th>
<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')} onClick={() => handleSort('totalOrders')}
> >
Satış{renderSortIndicator('totalOrders')} Satış{renderSortIndicator('totalOrders')}
</th> </th>
<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')} onClick={() => handleSort('totalRevenue')}
> >
Ciro{renderSortIndicator('totalRevenue')} Ciro{renderSortIndicator('totalRevenue')}
</th> </th>
<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')} onClick={() => handleSort('marketShare')}
> >
Pay %{renderSortIndicator('marketShare')} Pay %{renderSortIndicator('marketShare')}
</th> </th>
<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')} onClick={() => handleSort('avgPrice')}
> >
Ort. Fiyat{renderSortIndicator('avgPrice')} Ort. Fiyat{renderSortIndicator('avgPrice')}
</th> </th>
<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')} onClick={() => handleSort('categoryCount')}
> >
Kategori{renderSortIndicator('categoryCount')} Kategori{renderSortIndicator('categoryCount')}
</th> </th>
<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')} onClick={() => handleSort('brandCount')}
> >
Marka{renderSortIndicator('brandCount')} Marka{renderSortIndicator('brandCount')}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-slate-100">
{getSortedData().map((country) => ( {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"> <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>
<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 text-slate-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-slate-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 font-semibold text-slate-900">{country.totalRevenue.toLocaleString('tr-TR')}</td>
<td className="px-4 py-3 text-right text-sm"> <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"> <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)}% {country.marketShare.toFixed(1)}%
</span> </span>
</td> </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-right text-sm text-slate-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-slate-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-center text-sm text-slate-900">{country.brandCount}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -1,5 +1,8 @@
import { useState, useMemo, useEffect } from 'react' import { useState, useMemo, useEffect } from 'react'
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, ScatterChart, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, BarChart, Bar } from 'recharts' 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 }) { export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort, brandSortConfig }) {
// Pagination state // Pagination state
@@ -23,7 +26,7 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
if (!brandAnalytics) { if (!brandAnalytics) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
@@ -32,113 +35,73 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
<div className="space-y-6"> <div className="space-y-6">
{/* KPI Cards */} {/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <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"> <KpiCard
<div className="flex items-center justify-between"> title="Toplam Marka"
<div> value={brandAnalytics.kpis.totalBrands.toLocaleString('tr-TR')}
<p className="text-blue-100 text-sm font-medium">Toplam Marka</p> subtitle="Benzersiz marka sayısı"
<p className="text-3xl font-bold mt-2"> icon={Tag}
{brandAnalytics.kpis.totalBrands.toLocaleString('tr-TR')} color="blue"
</p> />
<p className="text-blue-100 text-xs mt-1">Benzersiz marka sayısı</p> <KpiCard
</div> title="Lider Marka Payı"
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3"> value={`%${brandAnalytics.kpis.leaderShare}`}
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> subtitle={brandAnalytics.topByOrders[0]?.name}
<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" /> icon={Trophy}
</svg> color="emerald"
</div> />
</div> <KpiCard
</div> title="Ort. Marka Başına Ürün"
value={brandAnalytics.kpis.avgProductsPerBrand}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white"> subtitle="Ürün çeşitliliği"
<div className="flex items-center justify-between"> icon={Package}
<div> color="violet"
<p className="text-green-100 text-sm font-medium">Lider Marka Payı</p> />
<p className="text-3xl font-bold mt-2"> <KpiCard
%{brandAnalytics.kpis.leaderShare} title="Pazar Yoğunlaşması"
</p> value={brandAnalytics.kpis.hhi}
<p className="text-green-100 text-xs mt-1">{brandAnalytics.topByOrders[0]?.name}</p> subtitle={`${brandAnalytics.kpis.marketConcentration} (HHI)`}
</div> icon={PieChartIcon}
<div className="bg-green-400 bg-opacity-30 rounded-full p-3"> color="orange"
<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>
</div> </div>
{/* Row 2: Top 20 Brands Full-Width Two-Column Table */} {/* Row 2: Top 20 Brands Full-Width Two-Column Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satan Markalar (Top 20)</h3> <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"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column - Brands 1-10 */} {/* Left Column - Brands 1-10 */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-50"> <thead className="bg-slate-50">
<tr> <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-slate-400">#</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-left text-xs font-medium text-slate-400">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-slate-400">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-slate-400">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-right text-xs font-medium text-slate-400">Pay %</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-slate-200">
{brandAnalytics.topByOrders.slice(0, 10).map((brand, index) => { {brandAnalytics.topByOrders.slice(0, 10).map((brand, index) => {
const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1) const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1)
return ( return (
<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-3 py-2 text-sm font-medium text-gray-900">{index + 1}</td> <td className="px-3 py-2 text-sm font-medium text-slate-900">{index + 1}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<a <a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {brand.name}
</a> </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>
<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')} {brand.totalOrders.toLocaleString('tr-TR')}
</td> </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')} {Math.round(brand.totalRevenue).toLocaleString('tr-TR')}
</td> </td>
<td className="px-3 py-2 text-right text-sm font-semibold text-green-600"> <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 */} {/* Right Column - Brands 11-20 */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-50"> <thead className="bg-slate-50">
<tr> <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-slate-400">#</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-left text-xs font-medium text-slate-400">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-slate-400">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-slate-400">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-right text-xs font-medium text-slate-400">Pay %</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-slate-200">
{brandAnalytics.topByOrders.slice(10, 20).map((brand, index) => { {brandAnalytics.topByOrders.slice(10, 20).map((brand, index) => {
const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1) const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1)
return ( return (
<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-3 py-2 text-sm font-medium text-gray-900">{index + 11}</td> <td className="px-3 py-2 text-sm font-medium text-slate-900">{index + 11}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<a <a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {brand.name}
</a> </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>
<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')} {brand.totalOrders.toLocaleString('tr-TR')}
</td> </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')} {Math.round(brand.totalRevenue).toLocaleString('tr-TR')}
</td> </td>
<td className="px-3 py-2 text-right text-sm font-semibold text-green-600"> <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 */} {/* Row 3: Market Share Chart & Price/Performance Matrix */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Market Share Pie Chart */} {/* Market Share Pie Chart */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Pazar Payı Dağılımı</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Pazar Payı Dağılımı</h3>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<PieChart> <PieChart>
<Pie <Pie
@@ -221,23 +184,23 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
fill="#8884d8" fill="#8884d8"
dataKey="value" 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} /> <Cell key={`cell-${index}`} fill={color} />
))} ))}
</Pie> </Pie>
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} /> <Tooltip formatter={(value) => value.toLocaleString('tr-TR')} {...CHART_TOOLTIP_STYLE} />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
<div className="mt-4 p-3 bg-blue-50 rounded"> <div className="mt-4 p-3 bg-slate-50 rounded-lg">
<p className="text-sm text-gray-700"> <p className="text-sm text-slate-700">
Top 3 marka pazarın <span className="font-bold text-blue-600">%{brandAnalytics.kpis.top3Share}</span>'ini kontrol ediyor Top 3 marka pazarın <span className="font-bold text-orange-500">%{brandAnalytics.kpis.top3Share}</span>'ini kontrol ediyor
</p> </p>
</div> </div>
</div> </div>
{/* Price/Quality Scatter Plot */} {/* Price/Quality Scatter Plot */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat/Performans Matrisi</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat/Performans Matrisi</h3>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}> <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
@@ -260,7 +223,7 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload const data = payload[0].payload
return ( 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="font-semibold">{data.name}</p>
<p className="text-sm">Fiyat: ₺{data.avgPrice.toLocaleString('tr-TR')}</p> <p className="text-sm">Fiyat: ₺{data.avgPrice.toLocaleString('tr-TR')}</p>
<p className="text-sm">Satış: {data.totalOrders.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 <Scatter
data={brandAnalytics.topByOrders.slice(0, 15)} data={brandAnalytics.topByOrders.slice(0, 15)}
fill="#3b82f6" fill={CHART_COLORS[0]}
opacity={0.6} opacity={0.6}
/> />
</ScatterChart> </ScatterChart>
</ResponsiveContainer> </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) Kabarcık boyutu: Sosyal kanıt skoru (görüntülenme + satış + ürün çeşitliliği)
</p> </p>
</div> </div>
</div> </div>
{/* Row 4: Detailed Brand Comparison Table */} {/* Row 4: Detailed Brand Comparison Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Marka Karşılaştırması</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Marka Karşılaştırması</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-50"> <thead className="bg-slate-50">
<tr> <tr>
<th <th
onClick={() => handleBrandSort('name')} 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"> <div className="flex items-center gap-1">
Marka Marka
{brandSortConfig.key === 'name' && ( {brandSortConfig.key === 'name' && (
<span className="text-gray-400"> <span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''} {brandSortConfig.direction === 'desc' ? '' : ''}
</span> </span>
)} )}
@@ -306,12 +269,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th> </th>
<th <th
onClick={() => handleBrandSort('totalOrders')} 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"> <div className="flex items-center justify-end gap-1">
Satış Satış
{brandSortConfig.key === 'totalOrders' && ( {brandSortConfig.key === 'totalOrders' && (
<span className="text-gray-400"> <span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''} {brandSortConfig.direction === 'desc' ? '' : ''}
</span> </span>
)} )}
@@ -319,12 +282,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th> </th>
<th <th
onClick={() => handleBrandSort('totalRevenue')} 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"> <div className="flex items-center justify-end gap-1">
Ciro Ciro
{brandSortConfig.key === 'totalRevenue' && ( {brandSortConfig.key === 'totalRevenue' && (
<span className="text-gray-400"> <span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''} {brandSortConfig.direction === 'desc' ? '' : ''}
</span> </span>
)} )}
@@ -332,12 +295,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th> </th>
<th <th
onClick={() => handleBrandSort('productCount')} 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"> <div className="flex items-center justify-end gap-1">
Ürün Sayısı Ürün Sayısı
{brandSortConfig.key === 'productCount' && ( {brandSortConfig.key === 'productCount' && (
<span className="text-gray-400"> <span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''} {brandSortConfig.direction === 'desc' ? '' : ''}
</span> </span>
)} )}
@@ -345,12 +308,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th> </th>
<th <th
onClick={() => handleBrandSort('categoryCount')} 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"> <div className="flex items-center justify-center gap-1">
Kategori Çeşitliliği Kategori Çeşitliliği
{brandSortConfig.key === 'categoryCount' && ( {brandSortConfig.key === 'categoryCount' && (
<span className="text-gray-400"> <span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''} {brandSortConfig.direction === 'desc' ? '' : ''}
</span> </span>
)} )}
@@ -358,41 +321,41 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</th> </th>
<th <th
onClick={() => handleBrandSort('avgPrice')} 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"> <div className="flex items-center justify-end gap-1">
Ort. Fiyat Ort. Fiyat
{brandSortConfig.key === 'avgPrice' && ( {brandSortConfig.key === 'avgPrice' && (
<span className="text-gray-400"> <span className="text-slate-400">
{brandSortConfig.direction === 'desc' ? '' : ''} {brandSortConfig.direction === 'desc' ? '' : ''}
</span> </span>
)} )}
</div> </div>
</th> </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-right text-xs font-medium text-slate-400">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-center text-xs font-medium text-slate-400">Segment</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-slate-200">
{paginatedBrands.map(brand => ( {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"> <td className="px-4 py-3">
<a <a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {brand.name}
</a> </a>
</td> </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')} {brand.totalOrders.toLocaleString('tr-TR')}
</td> </td>
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600"> <td className="px-4 py-3 text-right text-sm font-semibold text-green-600">
₺{brand.totalRevenue.toLocaleString('tr-TR')} ₺{brand.totalRevenue.toLocaleString('tr-TR')}
</td> </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} {brand.productCount}
</td> </td>
<td className="px-4 py-3 text-center"> <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> <span className="text-xs text-indigo-600">kategori</span>
</div> </div>
</td> </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')} ₺{brand.avgPrice.toLocaleString('tr-TR')}
</td> </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 !== brand.maxPrice
? `₺${brand.minPrice.toLocaleString('tr-TR')} - ₺${brand.maxPrice.toLocaleString('tr-TR')}` ? `₺${brand.minPrice.toLocaleString('tr-TR')} - ₺${brand.maxPrice.toLocaleString('tr-TR')}`
: `₺${brand.minPrice.toLocaleString('tr-TR')}` : `₺${brand.minPrice.toLocaleString('tr-TR')}`
@@ -429,23 +392,23 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {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="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) Sayfa {currentPage} / {totalPages} (Toplam {sortedBrands.length.toLocaleString('tr-TR')} marka)
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setCurrentPage(1)} onClick={() => setCurrentPage(1)}
disabled={currentPage === 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>
<button <button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))} onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 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 Önceki
</button> </button>
@@ -470,8 +433,8 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
onClick={() => setCurrentPage(pageNum)} onClick={() => setCurrentPage(pageNum)}
className={`px-3 py-1.5 text-sm border rounded-xl transition-colors ${ className={`px-3 py-1.5 text-sm border rounded-xl transition-colors ${
currentPage === pageNum currentPage === pageNum
? 'bg-blue-600 text-white border-blue-600' ? 'bg-orange-500 text-white border-orange-500'
: 'border-gray-300 hover:bg-gray-50' : 'border-slate-300 hover:bg-orange-50/30'
}`} }`}
> >
{pageNum} {pageNum}
@@ -483,14 +446,14 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
<button <button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))} onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === 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"
> >
Sonraki Sonraki
</button> </button>
<button <button
onClick={() => setCurrentPage(totalPages)} onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === 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> </button>
@@ -501,19 +464,19 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
</div> </div>
{/* Row 5: Market Insights */} {/* 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"> <div className="bg-slate-50 border border-slate-200 rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4"> <h3 className="text-lg font-semibold text-slate-900 mb-4">
Pazar İçgörüleri Pazar İçgörüleri
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Lider Marka Dominansı</p> <p className="text-sm font-medium text-slate-700">Lider Marka Dominansı</p>
<p className="text-lg font-bold text-blue-600 mt-1"> <p className="text-lg font-bold text-orange-500 mt-1">
{brandAnalytics.topByOrders[0]?.name} pazarın %{brandAnalytics.kpis.leaderShare}'ini kontrol ediyor {brandAnalytics.topByOrders[0]?.name} pazarın %{brandAnalytics.kpis.leaderShare}'ini kontrol ediyor
</p> </p>
</div> </div>
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Segment Analizi</p> <p className="text-sm font-medium text-slate-700">Segment Analizi</p>
<p className="text-lg font-bold text-purple-600 mt-1"> <p className="text-lg font-bold text-purple-600 mt-1">
{Object.entries(brandAnalytics.priceSegments) {Object.entries(brandAnalytics.priceSegments)
.sort((a, b) => b[1].length - a[1].length)[0][0]} .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) .sort((a, b) => b[1].length - a[1].length)[0][1].length} marka)
</p> </p>
</div> </div>
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Pazar Yapısı</p> <p className="text-sm font-medium text-slate-700">Pazar Yapısı</p>
<p className="text-lg font-bold text-green-600 mt-1"> <p className="text-lg font-bold text-green-600 mt-1">
{brandAnalytics.kpis.marketConcentration} - HHI: {brandAnalytics.kpis.hhi} {brandAnalytics.kpis.marketConcentration} - HHI: {brandAnalytics.kpis.hhi}
</p> </p>
</div> </div>
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Rekabet Yoğunluğu</p> <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"> <p className="text-lg font-bold text-orange-600 mt-1">
Top 3 marka toplam satışın %{brandAnalytics.kpis.top3Share}'i Top 3 marka toplam satışın %{brandAnalytics.kpis.top3Share}'i
</p> </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 { 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 }) { export default function CategoryTab({ categoryAnalytics, sortedCategories, handleCategorySort, categorySortConfig }) {
if (!categoryAnalytics) { if (!categoryAnalytics) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
@@ -13,95 +16,66 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
<div className="space-y-6"> <div className="space-y-6">
{/* Row 1: KPI Cards */} {/* Row 1: KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <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"> <KpiCard
<div className="flex items-center justify-between"> title="Toplam Kategori"
<div> value={categoryAnalytics.kpis.totalCategories}
<p className="text-blue-100 text-sm font-medium">Toplam Kategori</p> icon={Grid3X3}
<p className="text-3xl font-bold mt-2">{categoryAnalytics.kpis.totalCategories}</p> color="blue"
</div> />
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3"> <KpiCard
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> title="Lider Kategori Payı"
<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" /> value={`%${categoryAnalytics.kpis.leaderShare}`}
</svg> icon={Trophy}
</div> color="violet"
</div> />
</div> <KpiCard
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white"> title="Ort. Ürün/Kategori"
<div className="flex items-center justify-between"> value={categoryAnalytics.kpis.avgProductsPerCategory}
<div> icon={BarChart3}
<p className="text-purple-100 text-sm font-medium">Lider Kategori Payı</p> color="emerald"
<p className="text-3xl font-bold mt-2">%{categoryAnalytics.kpis.leaderShare}</p> />
</div> <KpiCard
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3"> title="Pazar Yoğunlaşması"
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> value={categoryAnalytics.kpis.marketConcentration}
<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" /> subtitle={`HHI: ${categoryAnalytics.kpis.hhi}`}
</svg> icon={PieChartIcon}
</div> color="orange"
</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>
</div> </div>
{/* Row 2: Top Categories Table + Price Positioning */} {/* Row 2: Top Categories Table + Price Positioning */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* En Çok Satan Kategoriler */} {/* En Çok Satan Kategoriler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satan Kategoriler (Top 20)</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satan Kategoriler (Top 20)</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="border-b border-gray-200"> <thead className="border-b border-slate-200">
<tr> <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-slate-400">#</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-left text-sm font-medium text-slate-400">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-slate-400">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-slate-400">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-right text-sm font-medium text-slate-400">Pay %</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-slate-100">
{categoryAnalytics.topByOrders.slice(0, 20).map((category, index) => ( {categoryAnalytics.topByOrders.slice(0, 20).map((category, index) => (
<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 text-sm text-gray-900 font-medium">{index + 1}</td> <td className="px-4 py-3 text-sm text-slate-900 font-medium">{index + 1}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<a <a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {category.name}
</a> </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>
<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-slate-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.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> <td className="px-4 py-3 text-right text-sm font-semibold text-green-600">{category.marketShare.toFixed(1)}%</td>
</tr> </tr>
))} ))}
@@ -111,11 +85,11 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</div> </div>
{/* Fiyat Pozisyonlaması */} {/* Fiyat Pozisyonlaması */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat Pozisyonlaması</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat Pozisyonlaması</h3>
<div className="space-y-4"> <div className="space-y-4">
{Object.entries(categoryAnalytics.priceSegments).map(([segment, categories]) => ( {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 justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`px-3 py-1 text-xs font-medium rounded-full ${ <span className={`px-3 py-1 text-xs font-medium rounded-full ${
@@ -127,15 +101,15 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
}`}> }`}>
{segment} {segment}
</span> </span>
<span className="text-sm font-semibold text-gray-700"> <span className="text-sm font-semibold text-slate-700">
{categories.length} kategori {categories.length} kategori
</span> </span>
</div> </div>
<span className="text-sm text-gray-600"> <span className="text-sm text-slate-500">
%{Math.round((categories.length / categoryAnalytics.categories.length) * 100)} %{Math.round((categories.length / categoryAnalytics.categories.length) * 100)}
</span> </span>
</div> </div>
<div className="w-full bg-gray-200 rounded-full h-2"> <div className="w-full bg-slate-200 rounded-full h-2">
<div <div
className={`h-2 rounded-full ${ className={`h-2 rounded-full ${
segment === 'Premium' segment === 'Premium'
@@ -147,7 +121,7 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
style={{ width: `${(categories.length / categoryAnalytics.categories.length) * 100}%` }} style={{ width: `${(categories.length / categoryAnalytics.categories.length) * 100}%` }}
></div> ></div>
</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')} Toplam satış: {categories.reduce((sum, c) => sum + c.totalOrders, 0).toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -159,8 +133,8 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
{/* Row 3: Market Share Chart + Price/Performance Matrix */} {/* Row 3: Market Share Chart + Price/Performance Matrix */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Pazar Payı Dağılımı */} {/* Pazar Payı Dağılımı */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kategori Pazar Payı Dağılımı</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Kategori Pazar Payı Dağılımı</h3>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<PieChart> <PieChart>
<Pie <Pie
@@ -185,21 +159,21 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
dataKey="value" dataKey="value"
> >
{[0, 1, 2, 3, 4, 5].map((index) => ( {[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> </Pie>
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} /> <Tooltip {...CHART_TOOLTIP_STYLE} formatter={(value) => value.toLocaleString('tr-TR')} />
<Legend /> <Legend />
</PieChart> </PieChart>
</ResponsiveContainer> </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 Pazar payı satış adedine göre hesaplanmıştır
</p> </p>
</div> </div>
{/* Fiyat/Performans Matrisi */} {/* Fiyat/Performans Matrisi */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat/Performans Matrisi</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat/Performans Matrisi</h3>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}> <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid /> <CartesianGrid />
@@ -212,11 +186,11 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload const data = payload[0].payload
return ( return (
<div className="bg-white p-3 border border-gray-300 rounded shadow-lg"> <div className="bg-slate-800 p-3 border-none rounded-xl shadow-lg">
<p className="font-semibold text-gray-900">{data.name}</p> <p className="font-semibold text-slate-50">{data.name}</p>
<p className="text-sm text-gray-600">Ort. Fiyat: {data.avgPrice.toLocaleString('tr-TR')}</p> <p className="text-sm text-slate-200">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-slate-200">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
<p className="text-sm text-gray-600">Sosyal Skor: {data.socialScore}</p> <p className="text-sm text-slate-200">Sosyal Skor: {data.socialScore}</p>
</div> </div>
) )
} }
@@ -227,27 +201,27 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
<Scatter name="Kategoriler" data={categoryAnalytics.topByOrders.slice(0, 15)} fill="#8b5cf6" /> <Scatter name="Kategoriler" data={categoryAnalytics.topByOrders.slice(0, 15)} fill="#8b5cf6" />
</ScatterChart> </ScatterChart>
</ResponsiveContainer> </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) Kabarcık boyutu: Sosyal kanıt skoru (görüntülenme + satış + ürün çeşitliliği)
</p> </p>
</div> </div>
</div> </div>
{/* Row 4: Detailed Category Comparison Table */} {/* Row 4: Detailed Category Comparison Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Kategori Karşılaştırması</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Kategori Karşılaştırması</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-50"> <thead className="bg-slate-50">
<tr> <tr>
<th <th
onClick={() => handleCategorySort('name')} 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"> <div className="flex items-center gap-1">
Kategori Kategori
{categorySortConfig.key === 'name' && ( {categorySortConfig.key === 'name' && (
<span className="text-gray-400"> <span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'} {categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span> </span>
)} )}
@@ -255,12 +229,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th> </th>
<th <th
onClick={() => handleCategorySort('totalOrders')} 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"> <div className="flex items-center justify-end gap-1">
Satış Satış
{categorySortConfig.key === 'totalOrders' && ( {categorySortConfig.key === 'totalOrders' && (
<span className="text-gray-400"> <span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'} {categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span> </span>
)} )}
@@ -268,12 +242,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th> </th>
<th <th
onClick={() => handleCategorySort('totalRevenue')} 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"> <div className="flex items-center justify-end gap-1">
Ciro Ciro
{categorySortConfig.key === 'totalRevenue' && ( {categorySortConfig.key === 'totalRevenue' && (
<span className="text-gray-400"> <span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'} {categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span> </span>
)} )}
@@ -281,12 +255,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th> </th>
<th <th
onClick={() => handleCategorySort('brandCount')} 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"> <div className="flex items-center justify-center gap-1">
Marka Çeşitliliği Marka Çeşitliliği
{categorySortConfig.key === 'brandCount' && ( {categorySortConfig.key === 'brandCount' && (
<span className="text-gray-400"> <span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'} {categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span> </span>
)} )}
@@ -294,35 +268,35 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</th> </th>
<th <th
onClick={() => handleCategorySort('avgPrice')} 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"> <div className="flex items-center justify-end gap-1">
Ort. Fiyat Ort. Fiyat
{categorySortConfig.key === 'avgPrice' && ( {categorySortConfig.key === 'avgPrice' && (
<span className="text-gray-400"> <span className="text-slate-400">
{categorySortConfig.direction === 'desc' ? '↓' : '↑'} {categorySortConfig.direction === 'desc' ? '↓' : '↑'}
</span> </span>
)} )}
</div> </div>
</th> </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-right text-xs font-medium text-slate-400">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-center text-xs font-medium text-slate-400">Segment</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-slate-200">
{sortedCategories.map(category => ( {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"> <td className="px-4 py-3">
<a <a
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {category.name}
</a> </a>
</td> </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')} {category.totalOrders.toLocaleString('tr-TR')}
</td> </td>
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600"> <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> <span className="text-xs text-indigo-600">marka</span>
</div> </div>
</td> </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')} {category.avgPrice.toLocaleString('tr-TR')}
</td> </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 !== category.maxPrice
? `${category.minPrice.toLocaleString('tr-TR')} - ₺${category.maxPrice.toLocaleString('tr-TR')}` ? `${category.minPrice.toLocaleString('tr-TR')} - ₺${category.maxPrice.toLocaleString('tr-TR')}`
: `${category.minPrice.toLocaleString('tr-TR')}` : `${category.minPrice.toLocaleString('tr-TR')}`
@@ -362,19 +336,19 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
</div> </div>
{/* Row 5: Category Insights */} {/* 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"> <div className="bg-slate-50 border border-slate-200 rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4"> <h3 className="text-lg font-semibold text-slate-900 mb-4">
Kategori İçgörüleri Kategori İçgörüleri
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Lider Kategori Dominansı</p> <p className="text-sm font-medium text-slate-700">Lider Kategori Dominansı</p>
<p className="text-lg font-bold text-blue-600 mt-1"> <p className="text-lg font-bold text-orange-500 mt-1">
{categoryAnalytics.topByOrders[0]?.name} pazarın %{categoryAnalytics.kpis.leaderShare}'ini kontrol ediyor {categoryAnalytics.topByOrders[0]?.name} pazarın %{categoryAnalytics.kpis.leaderShare}'ini kontrol ediyor
</p> </p>
</div> </div>
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Segment Analizi</p> <p className="text-sm font-medium text-slate-700">Segment Analizi</p>
<p className="text-lg font-bold text-purple-600 mt-1"> <p className="text-lg font-bold text-purple-600 mt-1">
{Object.entries(categoryAnalytics.priceSegments) {Object.entries(categoryAnalytics.priceSegments)
.sort((a, b) => b[1].length - a[1].length)[0][0]} .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) .sort((a, b) => b[1].length - a[1].length)[0][1].length} kategori)
</p> </p>
</div> </div>
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Pazar Yapısı</p> <p className="text-sm font-medium text-slate-700">Pazar Yapısı</p>
<p className="text-lg font-bold text-green-600 mt-1"> <p className="text-lg font-bold text-green-600 mt-1">
{categoryAnalytics.kpis.marketConcentration} - HHI: {categoryAnalytics.kpis.hhi} {categoryAnalytics.kpis.marketConcentration} - HHI: {categoryAnalytics.kpis.hhi}
</p> </p>
</div> </div>
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white rounded-xl p-4 shadow-sm">
<p className="text-sm font-medium text-gray-700">Rekabet Yoğunluğu</p> <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"> <p className="text-lg font-bold text-orange-600 mt-1">
Top 3 kategori toplam satışın %{categoryAnalytics.kpis.top3Share}'i Top 3 kategori toplam satışın %{categoryAnalytics.kpis.top3Share}'i
</p> </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 { useState } from 'react'
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ScatterChart, Scatter } from 'recharts' 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 }) { export default function OriginTab({ originAnalytics }) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: null }) 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') console.warn('⚠️ [ORIGINTAB] No originAnalytics data - showing loading state')
return ( return (
<div className="flex items-center justify-center h-64"> <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> </div>
) )
} }
@@ -116,82 +119,47 @@ export default function OriginTab({ originAnalytics }) {
<div className="space-y-6"> <div className="space-y-6">
{/* Row 1: KPI Cards - 4 cards in a grid */} {/* 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"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Card 1: Toplam Ülke Sayısı */} <KpiCard
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white"> title="Toplam Ülke Sayısı"
<div className="flex items-center justify-between"> value={countries?.length || 0}
<div> icon={Globe}
<p className="text-blue-100 text-sm font-medium">Toplam Ülke Sayısı</p> color="blue"
<p className="text-3xl font-bold mt-2">{countries?.length || 0}</p> />
</div> <KpiCard
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3"> title="Yerli Ürün Payı"
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> value={`${kpis?.domesticPercentage || 0}%`}
<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" /> subtitle={`${(domesticData?.count || 0).toLocaleString('tr-TR')} ürün`}
</svg> icon={Flag}
</div> color="emerald"
</div> />
</div> <KpiCard
title="İthal Ürün Payı"
{/* Card 2: Yerli Ürün Payı */} value={`${kpis?.importPercentage || 0}%`}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white"> subtitle={`${(importData?.count || 0).toLocaleString('tr-TR')} ürün`}
<div className="flex items-center justify-between"> icon={Ship}
<div> color="violet"
<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> <KpiCard
<p className="text-green-100 text-xs mt-1">{(domesticData?.count || 0).toLocaleString('tr-TR')} ürün</p> title="Toplam Satış"
</div> value={(totalOrders || 0).toLocaleString('tr-TR')}
<div className="bg-green-400 bg-opacity-30 rounded-full p-3"> subtitle="Tüm ülkeler"
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> icon={ShoppingBag}
<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" /> color="orange"
</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>
</div> </div>
{/* Row 2: Kategori-Ülke Isı Haritası (Top 10x10) - Full Width */} {/* Row 2: Kategori-Ülke Isı Haritası (Top 10x10) - Full Width */}
<div className="bg-white rounded-lg shadow-sm p-6"> <div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4"> <h3 className="text-lg font-semibold text-slate-800 mb-4">
Kategori-Ülke Isı Haritası (Top 10x10) Kategori-Ülke Isı Haritası (Top 10x10)
</h3> </h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr> <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 => ( {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} {country}
</th> </th>
))} ))}
@@ -200,7 +168,7 @@ export default function OriginTab({ originAnalytics }) {
<tbody> <tbody>
{topCategories.map(category => ( {topCategories.map(category => (
<tr key={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 => { {topCountriesForHeatmap.map(country => {
const cellData = categoryCountryMatrix[category]?.[country] const cellData = categoryCountryMatrix[category]?.[country]
const count = cellData?.count || 0 const count = cellData?.count || 0
@@ -217,7 +185,7 @@ export default function OriginTab({ originAnalytics }) {
else if (count > 0) bgColor = 'bg-blue-50' else if (count > 0) bgColor = 'bg-blue-50'
return ( 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 ? ( {count > 0 ? (
<div> <div>
<div className="text-xs font-semibold">Satış: {orders.toLocaleString('tr-TR')}</div> <div className="text-xs font-semibold">Satış: {orders.toLocaleString('tr-TR')}</div>
@@ -238,26 +206,26 @@ export default function OriginTab({ originAnalytics }) {
</div> </div>
{/* Row 3: En Çok Satan Ülkeler (Top 20) - Full Width, 10x10 Grid */} {/* Row 3: En Çok Satan Ülkeler (Top 20) - Full Width, 10x10 Grid */}
<div className="bg-white rounded-lg shadow-sm p-6"> <div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4"> <h3 className="text-lg font-semibold text-slate-800 mb-4">
En Çok Satan Ülkeler (Top 20) En Çok Satan Ülkeler (Top 20)
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* Left Column: Countries 1-10 */} {/* Left Column: Countries 1-10 */}
<div className="space-y-2"> <div className="space-y-2">
{(topByOrders || []).slice(0, 10).map((item, index) => ( {(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"> <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} {index + 1}
</span> </span>
<span className="font-semibold text-gray-800">{item.country}</span> <span className="font-semibold text-slate-800">{item.country}</span>
</div> </div>
<div className="text-right space-y-1"> <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')} <span className="font-semibold">Satış:</span> {item.totalOrders.toLocaleString('tr-TR')}
</p> </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')} <span className="font-semibold">Ciro:</span> {item.totalRevenue.toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -268,18 +236,18 @@ export default function OriginTab({ originAnalytics }) {
{/* Right Column: Countries 11-20 */} {/* Right Column: Countries 11-20 */}
<div className="space-y-2"> <div className="space-y-2">
{(topByOrders || []).slice(10, 20).map((item, index) => ( {(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"> <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"> <span className="flex items-center justify-center w-8 h-8 bg-purple-500 text-white rounded-full font-bold text-sm">
{index + 11} {index + 11}
</span> </span>
<span className="font-semibold text-gray-800">{item.country}</span> <span className="font-semibold text-slate-800">{item.country}</span>
</div> </div>
<div className="text-right space-y-1"> <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')} <span className="font-semibold">Satış:</span> {item.totalOrders.toLocaleString('tr-TR')}
</p> </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')} <span className="font-semibold">Ciro:</span> {item.totalRevenue.toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -292,8 +260,8 @@ export default function OriginTab({ originAnalytics }) {
{/* Row 4: Two Charts Side by Side (50%-50%) */} {/* Row 4: Two Charts Side by Side (50%-50%) */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Chart 1: Ülke Bazlı Satış (Bar Chart) */} {/* Chart 1: Ülke Bazlı Satış (Bar Chart) */}
<div className="bg-white rounded-lg shadow-sm p-6"> <div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4"> <h3 className="text-lg font-semibold text-slate-800 mb-4">
Ülke Bazlı Satış (Top 15) Ülke Bazlı Satış (Top 15)
</h3> </h3>
<ResponsiveContainer width="100%" height={400}> <ResponsiveContainer width="100%" height={400}>
@@ -308,22 +276,17 @@ export default function OriginTab({ originAnalytics }) {
/> />
<YAxis tick={{ fontSize: 11 }} /> <YAxis tick={{ fontSize: 11 }} />
<Tooltip <Tooltip
contentStyle={{ {...CHART_TOOLTIP_STYLE}
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
fontSize: '12px'
}}
formatter={(value) => value.toLocaleString('tr-TR')} 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> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
{/* Chart 2: Ortalama Fiyat / Ciro İlişkisi (Scatter Chart) */} {/* Chart 2: Ortalama Fiyat / Ciro İlişkisi (Scatter Chart) */}
<div className="bg-white rounded-lg shadow-sm p-6"> <div className="bg-white rounded-xl shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4"> <h3 className="text-lg font-semibold text-slate-800 mb-4">
Ciro / Satış İlişkisi (Top 15) Ciro / Satış İlişkisi (Top 15)
</h3> </h3>
<ResponsiveContainer width="100%" height={400}> <ResponsiveContainer width="100%" height={400}>
@@ -349,12 +312,12 @@ export default function OriginTab({ originAnalytics }) {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload[0].payload const data = payload[0].payload
return ( return (
<div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3"> <div className="bg-slate-800 border-none rounded-xl shadow-lg p-3">
<p className="font-semibold text-gray-900 mb-2 text-sm">{data.name}</p> <p className="font-semibold text-slate-50 mb-2 text-sm">{data.name}</p>
<p className="font-semibold text-gray-900 text-sm"> <p className="font-semibold text-slate-200 text-sm">
Ciro: {data.totalRevenue.toLocaleString('tr-TR')} Ciro: {data.totalRevenue.toLocaleString('tr-TR')}
</p> </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')} Satış: {data.totalOrders.toLocaleString('tr-TR')}
</p> </p>
</div> </div>
@@ -377,70 +340,70 @@ export default function OriginTab({ originAnalytics }) {
</div> </div>
{/* Row 5: Detaylı Ülke Karşılaştırma Tablosu */} {/* Row 5: Detaylı Ülke Karşılaştırma Tablosu */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Ülke Karşılaştırma</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Ülke Karşılaştırma</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <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> <tr>
<th <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')} onClick={() => handleSort('country')}
> >
Ülke{renderSortIndicator('country')} Ülke{renderSortIndicator('country')}
</th> </th>
<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')} onClick={() => handleSort('count')}
> >
Ürün{renderSortIndicator('count')} Ürün{renderSortIndicator('count')}
</th> </th>
<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')} onClick={() => handleSort('totalOrders')}
> >
Satış{renderSortIndicator('totalOrders')} Satış{renderSortIndicator('totalOrders')}
</th> </th>
<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')} onClick={() => handleSort('totalRevenue')}
> >
Ciro{renderSortIndicator('totalRevenue')} Ciro{renderSortIndicator('totalRevenue')}
</th> </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 <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')} onClick={() => handleSort('avgPrice')}
> >
Ort. Fiyat{renderSortIndicator('avgPrice')} Ort. Fiyat{renderSortIndicator('avgPrice')}
</th> </th>
<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')} onClick={() => handleSort('categoryCount')}
> >
Kategori{renderSortIndicator('categoryCount')} Kategori{renderSortIndicator('categoryCount')}
</th> </th>
<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')} onClick={() => handleSort('brandCount')}
> >
Marka{renderSortIndicator('brandCount')} Marka{renderSortIndicator('brandCount')}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-slate-100">
{getSortedData().map((item) => ( {getSortedData().map((item) => (
<tr key={item.country} className="hover:bg-gray-50"> <tr key={item.country} className="hover:bg-orange-50/30">
<td className="px-4 py-3 text-sm font-semibold text-gray-900"> <td className="px-4 py-3 text-sm font-semibold text-slate-900">
{item.country} {item.country}
</td> </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')} {item.count.toLocaleString('tr-TR')}
</td> </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')} {item.totalOrders.toLocaleString('tr-TR')}
</td> </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')} {item.totalRevenue.toLocaleString('tr-TR')}
</td> </td>
<td className="px-4 py-3 text-sm text-right"> <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)}% {((item.totalOrders / totalOrders) * 100).toFixed(1)}%
</span> </span>
</td> </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 })} {item.avgPrice.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}
</td> </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-slate-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.brandCount || 0}</td>
</tr> </tr>
))} ))}
</tbody> </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({ export default function OverviewTab({
overviewKPIs, overviewKPIs,
topSellingProducts, topSellingProducts,
topSellingBrands, topSellingBrands,
topSellingCategories, 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) { if (!overviewKPIs) {
return ( return (
<div className="flex items-center justify-center h-64"> <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> </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 ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* KPI Cards */} {/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4"> <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"> <KpiCard
<div className="flex items-center justify-between"> title="Toplam Ürün"
<div> value={overviewKPIs.totalProducts.toLocaleString('tr-TR')}
<p className="text-blue-100 text-sm font-medium">Toplam Ürün</p> icon={Package}
<p className="text-3xl font-bold mt-2"> color="blue"
{overviewKPIs.totalProducts.toLocaleString('tr-TR')} />
</p> <KpiCard
</div> title="Toplam Satın Alma"
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3"> value={overviewKPIs.totalOrders.toLocaleString('tr-TR')}
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> icon={ShoppingCart}
<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" /> color="emerald"
</svg> />
</div> <KpiCard
</div> title="Toplam Görüntülenme"
</div> value={overviewKPIs.totalViews.toLocaleString('tr-TR')}
icon={Eye}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white"> color="violet"
<div className="flex items-center justify-between"> />
<div> <KpiCard
<p className="text-green-100 text-sm font-medium">Toplam Satın Alma</p> title="Toplam Ciro"
<p className="text-3xl font-bold mt-2"> value={`${(overviewKPIs.totalRevenue || 0).toLocaleString('tr-TR')}`}
{overviewKPIs.totalOrders.toLocaleString('tr-TR')} icon={DollarSign}
</p> color="orange"
</div> />
<div className="bg-green-400 bg-opacity-30 rounded-full p-3"> <KpiCard
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"> title="Ortalama Fiyat"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /> value={`${overviewKPIs.avgPrice.toLocaleString('tr-TR')}`}
</svg> icon={Tag}
</div> color="rose"
</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>
</div> </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 */} {/* Row 2: Top Products & Top Brands */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* En Çok Satış Yapan Ürünler */} {/* En Çok Satış Yapan Ürünler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Ürünler</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Ürünler</h3>
<div className="space-y-3"> <div className="space-y-3">
{topSellingProducts.map((product, index) => ( {topSellingProducts.map((product, index) => (
<a <a
@@ -111,9 +383,9 @@ export default function OverviewTab({
href={product.url} href={product.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {index + 1}
</div> </div>
<img <img
@@ -122,12 +394,12 @@ export default function OverviewTab({
className="w-16 h-16 object-cover rounded" className="w-16 h-16 object-cover rounded"
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{product.name}</p> <p className="text-sm font-medium text-slate-900 truncate">{product.name}</p>
<p className="text-xs text-gray-600">{product.price?.toLocaleString('tr-TR')}</p> <p className="text-xs text-slate-500">{product.price?.toLocaleString('tr-TR')}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-bold text-gray-900">{product.orders?.toLocaleString('tr-TR')}</p> <p className="text-sm font-bold text-slate-900">{product.orders?.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">satış</p> <p className="text-xs text-slate-500">satış</p>
</div> </div>
</a> </a>
))} ))}
@@ -135,8 +407,8 @@ export default function OverviewTab({
</div> </div>
{/* En Çok Satış Yapan Marka */} {/* En Çok Satış Yapan Marka */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Marka</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Marka</h3>
<div className="space-y-3"> <div className="space-y-3">
{topSellingBrands.map((brand, index) => ( {topSellingBrands.map((brand, index) => (
<a <a
@@ -144,17 +416,17 @@ export default function OverviewTab({
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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"> <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} {index + 1}
</div> </div>
<div className="flex-1"> <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>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-bold text-gray-900">{brand.totalOrders.toLocaleString('tr-TR')}</p> <p className="text-sm font-bold text-slate-900">{brand.totalOrders.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">toplam satış</p> <p className="text-xs text-slate-500">toplam satış</p>
</div> </div>
</a> </a>
))} ))}
@@ -165,8 +437,8 @@ export default function OverviewTab({
{/* Row 3: Top Categories by Revenue & Most Viewed Categories */} {/* Row 3: Top Categories by Revenue & Most Viewed Categories */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* En Çok Satış Yapan Kategoriler */} {/* En Çok Satış Yapan Kategoriler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Kategoriler</h3> <h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Kategoriler</h3>
<div className="space-y-3"> <div className="space-y-3">
{topSellingCategories.map((category, index) => ( {topSellingCategories.map((category, index) => (
<a <a
@@ -174,17 +446,17 @@ export default function OverviewTab({
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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"> <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} {index + 1}
</div> </div>
<div className="flex-1"> <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>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-bold text-gray-900">{category.revenue.toLocaleString('tr-TR')}</p> <p className="text-sm font-bold text-slate-900">{category.revenue.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">toplam ciro</p> <p className="text-xs text-slate-500">toplam ciro</p>
</div> </div>
</a> </a>
))} ))}
@@ -192,8 +464,8 @@ export default function OverviewTab({
</div> </div>
{/* En Çok Görüntülenme Alan Kategoriler */} {/* En Çok Görüntülenme Alan Kategoriler */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Görüntülenme Alan Kategoriler</h3> <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"> <div className="space-y-3">
{mostViewedCategories.map((category, index) => ( {mostViewedCategories.map((category, index) => (
<a <a
@@ -201,17 +473,17 @@ export default function OverviewTab({
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`} href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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"> <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} {index + 1}
</div> </div>
<div className="flex-1"> <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>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-bold text-gray-900">{category.views.toLocaleString('tr-TR')}</p> <p className="text-sm font-bold text-slate-900">{category.views.toLocaleString('tr-TR')}</p>
<p className="text-xs text-gray-600">görüntülenme</p> <p className="text-xs text-slate-500">görüntülenme</p>
</div> </div>
</a> </a>
))} ))}

View File

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

View File

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

34778
categories/Bebek_104158.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

32903
categories/Giyim_82.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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