mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-07-01 01:17:04 +00:00
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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -107,3 +107,8 @@ README_TESTING.md
|
||||
# Backend debug/analysis artifacts
|
||||
backend/*_202*.json
|
||||
backend/*_202*.png
|
||||
|
||||
# Screenshots and test images
|
||||
*.png
|
||||
*.jpeg
|
||||
*.jpg
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>admin-panel</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<title>Trendyol Analytics</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
116
admin-panel/package-lock.json
generated
116
admin-panel/package-lock.json
generated
@@ -9,10 +9,12 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.4.1"
|
||||
"recharts": "^3.4.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.3",
|
||||
@@ -2269,6 +2271,15 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -2527,6 +2538,19 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2593,6 +2617,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2658,6 +2691,18 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3329,6 +3374,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -4069,6 +4123,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.577.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
|
||||
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -4744,6 +4807,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -5004,6 +5079,24 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -5021,6 +5114,27 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.4.1"
|
||||
"recharts": "^3.4.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.3",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,98 +1,35 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Routes, Route, NavLink } from 'react-router-dom'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Layout from './components/ui/Layout'
|
||||
import { PageSkeleton } from './components/ui/SkeletonLoader'
|
||||
|
||||
// OPTIMIZATION: Lazy load components for 65% smaller initial bundle (500KB → 175KB)
|
||||
// OPTIMIZATION: Lazy load components for smaller initial bundle
|
||||
// Components are loaded only when user navigates to them
|
||||
const CategoryManagement = lazy(() => import('./components/CategoryManagement'))
|
||||
const ReportGeneration = lazy(() => import('./components/ReportGeneration'))
|
||||
const ReportList = lazy(() => import('./components/ReportList'))
|
||||
const ReportDashboard = lazy(() => import('./components/ReportDashboard'))
|
||||
const ReportComparison = lazy(() => import('./components/ReportComparison'))
|
||||
|
||||
// Loading component
|
||||
// Skeleton loading fallback
|
||||
const LoadingFallback = () => (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
|
||||
<p className="text-gray-600">Yükleniyor...</p>
|
||||
</div>
|
||||
<div className="page-enter page-enter-active">
|
||||
<PageSkeleton />
|
||||
</div>
|
||||
)
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Trendyol Admin Panel</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">Kategori Yönetimi & Veri Analizi</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Tabs - Only show on non-report pages */}
|
||||
<Routes>
|
||||
<Route path="/reports/:reportId" element={null} />
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<div className="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div className="container mx-auto px-6">
|
||||
<nav className="flex space-x-8">
|
||||
<NavLink
|
||||
to="/"
|
||||
className={({ isActive }) =>
|
||||
`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||||
isActive
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`
|
||||
}
|
||||
>
|
||||
Kategori Yönetimi
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/report"
|
||||
className={({ isActive }) =>
|
||||
`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||||
isActive
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`
|
||||
}
|
||||
>
|
||||
Rapor Oluştur
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/reports"
|
||||
className={({ isActive }) =>
|
||||
`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||||
isActive
|
||||
? 'border-blue-600 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`
|
||||
}
|
||||
>
|
||||
Raporlarım
|
||||
</NavLink>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
{/* Content - Wrapped in Suspense for lazy loading */}
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
<Layout>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<CategoryManagement />} />
|
||||
<Route path="/" element={<ReportGeneration />} />
|
||||
<Route path="/report" element={<ReportGeneration />} />
|
||||
<Route path="/reports" element={<ReportList />} />
|
||||
<Route path="/reports/:reportId" element={<ReportDashboard />} />
|
||||
<Route path="/compare" element={<ReportComparison />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { API_URL, fetchWithTimeout } from '../config/api'
|
||||
import { getCategoryIcon, getCategoryColors } from '../constants/categories'
|
||||
import { FolderTree, ChevronRight, ExternalLink, Layers, Loader2, FolderOpen, ArrowLeft } from 'lucide-react'
|
||||
|
||||
function CategoryManagement() {
|
||||
const [mainCategories, setMainCategories] = useState([])
|
||||
@@ -9,6 +9,8 @@ function CategoryManagement() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingSubCategories, setLoadingSubCategories] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
// Breadcrumb trail for deep navigation
|
||||
const [breadcrumb, setBreadcrumb] = useState([])
|
||||
|
||||
// Fetch main categories on mount
|
||||
useEffect(() => {
|
||||
@@ -46,13 +48,39 @@ function CategoryManagement() {
|
||||
|
||||
const handleCategoryClick = (category) => {
|
||||
setSelectedCategory(category)
|
||||
setBreadcrumb([category])
|
||||
fetchSubCategories(category.id)
|
||||
}
|
||||
|
||||
const handleSubCategoryClick = (subCat) => {
|
||||
if (subCat.children_count > 0) {
|
||||
setBreadcrumb(prev => [...prev, subCat])
|
||||
setSelectedCategory(subCat)
|
||||
fetchSubCategories(subCat.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBreadcrumbClick = (index) => {
|
||||
const target = breadcrumb[index]
|
||||
// Trim breadcrumb to clicked level
|
||||
setBreadcrumb(breadcrumb.slice(0, index + 1))
|
||||
setSelectedCategory(target)
|
||||
fetchSubCategories(target.id)
|
||||
}
|
||||
|
||||
const handleBackToMain = () => {
|
||||
setSelectedCategory(null)
|
||||
setBreadcrumb([])
|
||||
setSubCategories([])
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Yükleniyor...</div>
|
||||
<div className="text-slate-400 flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Yükleniyor...
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -68,94 +96,150 @@ function CategoryManagement() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Ana Kategoriler</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h1 className="text-2xl font-bold text-slate-900">Kategori Yönetimi</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Toplam {mainCategories.length} ana kategori
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Categories Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{mainCategories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
className={`bg-white border-l-4 ${selectedCategory?.id === category.id ? 'border-blue-600' : 'border-gray-300'} rounded-lg shadow-sm p-6 cursor-pointer hover:shadow-md transition-shadow ${
|
||||
selectedCategory?.id === category.id ? 'ring-2 ring-blue-200' : ''
|
||||
}`}
|
||||
className={`bg-white border-l-4 ${selectedCategory?.id === category.id && breadcrumb.length === 1 ? 'border-orange-500 ring-2 ring-orange-200' : 'border-slate-300'} rounded-xl shadow-sm p-5 cursor-pointer hover:shadow-md transition-all`}
|
||||
>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{category.name}</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">{category.name}</h3>
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
{category.children_count || 0} alt kategori
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-slate-300" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Subcategories Table */}
|
||||
{/* Subcategories Panel */}
|
||||
{selectedCategory && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
{selectedCategory.name} - Alt Kategoriler
|
||||
</h2>
|
||||
<span className="text-sm text-gray-600">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
{/* Breadcrumb Navigation */}
|
||||
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-slate-200">
|
||||
<button
|
||||
onClick={handleBackToMain}
|
||||
className="text-slate-400 hover:text-orange-500 transition-colors"
|
||||
title="Ana kategorilere dön"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<nav className="flex items-center gap-1 text-sm flex-wrap">
|
||||
{breadcrumb.map((crumb, index) => (
|
||||
<span key={crumb.id} className="flex items-center gap-1">
|
||||
{index > 0 && <ChevronRight className="w-4 h-4 text-slate-300" />}
|
||||
{index < breadcrumb.length - 1 ? (
|
||||
<button
|
||||
onClick={() => handleBreadcrumbClick(index)}
|
||||
className="text-orange-500 hover:text-orange-600 hover:underline font-medium"
|
||||
>
|
||||
{crumb.name}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-slate-900 font-semibold">{crumb.name}</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<span className="ml-auto text-sm text-slate-400">
|
||||
{subCategories.length} kategori
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loadingSubCategories ? (
|
||||
<div className="text-center py-8 text-gray-500">Yükleniyor...</div>
|
||||
<div className="text-center py-8 text-slate-400 flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Yükleniyor...
|
||||
</div>
|
||||
) : subCategories.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="text-center py-8 text-slate-400">
|
||||
Bu kategoride alt kategori bulunamadı.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-slate-200">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
|
||||
#
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
|
||||
Kategori Adı
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
|
||||
Alt Kategori
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
|
||||
Trendyol ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider">
|
||||
Trendyol URL
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-white divide-y divide-slate-200">
|
||||
{subCategories.map((subCat, index) => (
|
||||
<tr key={subCat.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<tr
|
||||
key={subCat.id}
|
||||
onClick={() => handleSubCategoryClick(subCat)}
|
||||
className={`hover:bg-orange-50/30 ${subCat.children_count > 0 ? 'cursor-pointer' : ''}`}
|
||||
>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
|
||||
{index + 1}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">
|
||||
<div className="flex items-center gap-2">
|
||||
{subCat.children_count > 0 ? (
|
||||
<FolderOpen className="w-4 h-4 text-orange-400" />
|
||||
) : (
|
||||
<Layers className="w-4 h-4 text-slate-300" />
|
||||
)}
|
||||
{subCat.name}
|
||||
{subCat.children_count > 0 && (
|
||||
<ChevronRight className="w-4 h-4 text-orange-400" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{subCat.trendyol_category_id || '-'}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
|
||||
{subCat.children_count > 0 ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-50 text-orange-600 border border-orange-200">
|
||||
{subCat.children_count} alt kategori
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-400">
|
||||
{subCat.trendyol_category_id || '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-400">
|
||||
{subCat.trendyol_url ? (
|
||||
<a
|
||||
href={subCat.trendyol_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline"
|
||||
className="text-orange-500 hover:underline inline-flex items-center gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Trendyol'da Aç
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-400">Link yok</span>
|
||||
<span className="text-slate-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
280
admin-panel/src/components/ReportComparison.jsx
Normal file
280
admin-panel/src/components/ReportComparison.jsx
Normal 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
|
||||
@@ -3,6 +3,8 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { TAB_GROUPS, ALL_TABS } from '../constants/tabGroups'
|
||||
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../config/api'
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend, ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, ZAxis, BarChart, Bar } from 'recharts'
|
||||
import { ArrowLeft, BarChart3, Award, Grid3X3, Globe, Barcode, Key, Search, Trophy, Target, Download, Printer } from 'lucide-react'
|
||||
import { PageSkeleton } from './ui/SkeletonLoader'
|
||||
import BarcodeTab from './dashboard-tabs/BarcodeTab'
|
||||
import OriginTab from './dashboard-tabs/OriginTab'
|
||||
import OverviewTab from './dashboard-tabs/OverviewTab'
|
||||
@@ -10,6 +12,9 @@ import BrandTab from './dashboard-tabs/BrandTab'
|
||||
import CategoryTab from './dashboard-tabs/CategoryTab'
|
||||
import KeywordTab from './dashboard-tabs/KeywordTab'
|
||||
import ProductFinderTab from './dashboard-tabs/ProductFinderTab'
|
||||
import HiddenChampionsTab from './dashboard-tabs/HiddenChampionsTab'
|
||||
import OpportunityTab from './dashboard-tabs/OpportunityTab'
|
||||
import { exportToExcel, printReport } from '../utils/exportUtils'
|
||||
|
||||
function ReportDashboard() {
|
||||
const { reportId } = useParams()
|
||||
@@ -1126,14 +1131,7 @@ function ReportDashboard() {
|
||||
}, [dashboardData])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Dashboard yükleniyor...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <PageSkeleton />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
@@ -1141,11 +1139,11 @@ function ReportDashboard() {
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="text-red-500 text-6xl mb-4">⚠️</div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">Hata</h2>
|
||||
<p className="text-gray-600 mb-4">{error}</p>
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">Hata</h2>
|
||||
<p className="text-slate-500 mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => navigate('/reports')}
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
||||
className="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600"
|
||||
>
|
||||
Raporlara Dön
|
||||
</button>
|
||||
@@ -1155,40 +1153,74 @@ function ReportDashboard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<div className="min-h-screen bg-slate-100">
|
||||
<div className="w-full px-4 py-6">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{dashboardData.report_name}</h1>
|
||||
<p className="text-gray-600 mt-1">{dashboardData.category_name}</p>
|
||||
<h1 className="text-3xl font-bold text-slate-900">{dashboardData.report_name}</h1>
|
||||
<p className="text-slate-500 mt-1">{dashboardData.category_name}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => exportToExcel(dashboardData, dashboardData.report_name)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-emerald-600 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors text-sm font-medium"
|
||||
title="Excel İndir"
|
||||
>
|
||||
<Download size={16} />
|
||||
Excel
|
||||
</button>
|
||||
<button
|
||||
onClick={printReport}
|
||||
className="flex items-center gap-2 px-4 py-2 text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors text-sm font-medium"
|
||||
title="Yazdır"
|
||||
>
|
||||
<Printer size={16} />
|
||||
Yazdır
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/reports')}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
className="flex items-center gap-2 px-4 py-2 text-slate-500 hover:text-slate-700 hover:bg-orange-50/30 rounded-lg transition-colors"
|
||||
>
|
||||
← Geri
|
||||
<ArrowLeft size={18} />
|
||||
Geri
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 mb-6">
|
||||
<div className="flex border-b border-gray-200 overflow-x-auto">
|
||||
{ALL_TABS.map(tab => (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 mb-6 p-3">
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
{ALL_TABS.map(tab => {
|
||||
const TAB_ICONS = {
|
||||
'overview': BarChart3,
|
||||
'brand': Award,
|
||||
'category': Grid3X3,
|
||||
'origin': Globe,
|
||||
'barcode': Barcode,
|
||||
'keyword': Key,
|
||||
'product-finder': Search,
|
||||
'hidden-champions': Trophy,
|
||||
'opportunity': Target
|
||||
}
|
||||
const TabIcon = TAB_ICONS[tab.id] || BarChart3
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-6 py-4 font-medium whitespace-nowrap transition-colors ${
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all ${
|
||||
activeTab === tab.id
|
||||
? 'border-b-2 border-blue-500 text-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
|
||||
? 'bg-orange-500 text-white shadow-sm'
|
||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<TabIcon size={16} />
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1202,6 +1234,8 @@ function ReportDashboard() {
|
||||
topSellingBrands={topSellingBrands}
|
||||
topSellingCategories={topSellingCategories}
|
||||
mostViewedCategories={mostViewedCategories}
|
||||
reportId={reportId}
|
||||
allProducts={dashboardData?.all_products || []}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1240,6 +1274,16 @@ function ReportDashboard() {
|
||||
{activeTab === 'product-finder' && (
|
||||
<ProductFinderTab allProducts={dashboardData.all_products || []} />
|
||||
)}
|
||||
|
||||
{/* GİZLİ ŞAMPİYONLAR TAB */}
|
||||
{activeTab === 'hidden-champions' && (
|
||||
<HiddenChampionsTab reportId={reportId} />
|
||||
)}
|
||||
|
||||
{/* FIRSAT HARİTASI TAB */}
|
||||
{activeTab === 'opportunity' && (
|
||||
<OpportunityTab allProducts={dashboardData?.all_products || []} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG, calculateNextDelay } from '../config/api'
|
||||
import { Search, Loader2, ChevronRight, ArrowLeft, X, Check } from 'lucide-react'
|
||||
|
||||
function ReportGeneration() {
|
||||
const [mainCategories, setMainCategories] = useState([])
|
||||
const [selectedCategory, setSelectedCategory] = useState(null)
|
||||
const [subCategories, setSubCategories] = useState([])
|
||||
const [selectedSubCategories, setSelectedSubCategories] = useState([]) // Changed to array for multi-select
|
||||
const [selectedSubCategories, setSelectedSubCategories] = useState([])
|
||||
const [loadingSubCategories, setLoadingSubCategories] = useState(false)
|
||||
const [subCategorySearch, setSubCategorySearch] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -18,6 +19,8 @@ function ReportGeneration() {
|
||||
const [error, setError] = useState(null)
|
||||
const [showCompletionModal, setShowCompletionModal] = useState(false)
|
||||
const [completionData, setCompletionData] = useState(null)
|
||||
const [subBreadcrumb, setSubBreadcrumb] = useState([])
|
||||
const [showSearch, setShowSearch] = useState(false)
|
||||
const pollTimeoutRef = useRef(null)
|
||||
const isMountedRef = useRef(true)
|
||||
const logsEndRef = useRef(null)
|
||||
@@ -34,7 +37,6 @@ function ReportGeneration() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Auto-scroll to bottom of logs
|
||||
useEffect(() => {
|
||||
if (logsEndRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
@@ -62,11 +64,15 @@ function ReportGeneration() {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSubCategories = async (categoryId) => {
|
||||
const fetchSubCategories = async (categoryId, resetSelection = true) => {
|
||||
setLoadingSubCategories(true)
|
||||
setSubCategories([])
|
||||
setSelectedSubCategories([]) // Clear selected subcategories
|
||||
setSubCategorySearch('') // Clear search when loading new category
|
||||
if (resetSelection) {
|
||||
setSelectedSubCategories([])
|
||||
setSubBreadcrumb([])
|
||||
}
|
||||
setSubCategorySearch('')
|
||||
setShowSearch(false)
|
||||
try {
|
||||
const response = await fetchWithTimeout(`${API_URL}/categories/${categoryId}/children`)
|
||||
if (!response.ok) throw new Error('Failed to fetch sub-categories')
|
||||
@@ -85,6 +91,32 @@ function ReportGeneration() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrillDown = (subCat) => {
|
||||
if (generating || subCat.children_count === 0) return
|
||||
setSubBreadcrumb(prev => [...prev, subCat])
|
||||
fetchSubCategories(subCat.id, false)
|
||||
}
|
||||
|
||||
const handleBreadcrumbBack = (index) => {
|
||||
if (index === -1) {
|
||||
setSubBreadcrumb([])
|
||||
fetchSubCategories(selectedCategory.id, false)
|
||||
} else {
|
||||
const target = subBreadcrumb[index]
|
||||
setSubBreadcrumb(subBreadcrumb.slice(0, index + 1))
|
||||
fetchSubCategories(target.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSubCategory = (subCat) => {
|
||||
if (generating) return
|
||||
setSelectedSubCategories(prev => {
|
||||
const exists = prev.some(c => c.id === subCat.id)
|
||||
if (exists) return prev.filter(c => c.id !== subCat.id)
|
||||
return [...prev, subCat]
|
||||
})
|
||||
}
|
||||
|
||||
const addLog = (message, type = 'info') => {
|
||||
const timestamp = new Date().toLocaleTimeString('tr-TR')
|
||||
setLogs((prev) => [...prev, { timestamp, message, type }])
|
||||
@@ -95,8 +127,6 @@ function ReportGeneration() {
|
||||
alert('Lütfen önce bir kategori seçin!')
|
||||
return
|
||||
}
|
||||
|
||||
// Show modal first to get report name
|
||||
setReportName(`${new Date().toLocaleDateString('tr-TR', { month: 'long' })} ${selectedCategory.name} Raporu`)
|
||||
setShowNameModal(true)
|
||||
}
|
||||
@@ -113,13 +143,11 @@ function ReportGeneration() {
|
||||
setLogs([])
|
||||
|
||||
try {
|
||||
// Prepare request body
|
||||
const requestBody = {
|
||||
name: reportName,
|
||||
category_id: selectedCategory.id
|
||||
}
|
||||
|
||||
// Add subcategory_ids if subcategories are selected
|
||||
if (selectedSubCategories.length > 0) {
|
||||
requestBody.subcategory_ids = selectedSubCategories.map(cat => cat.id)
|
||||
}
|
||||
@@ -129,10 +157,6 @@ function ReportGeneration() {
|
||||
console.log(' - Kategori ID:', requestBody.category_id)
|
||||
console.log(' - Alt kategori IDs:', requestBody.subcategory_ids)
|
||||
|
||||
// Build URL with query parameters for POST request with streaming
|
||||
const url = new URL(`${API_URL}/api/reports/create`)
|
||||
|
||||
// Build SSE URL
|
||||
const params = new URLSearchParams({
|
||||
name: requestBody.name,
|
||||
category_id: requestBody.category_id,
|
||||
@@ -142,7 +166,6 @@ function ReportGeneration() {
|
||||
const sseUrl = `${API_URL}/api/reports/create?${params}`
|
||||
console.log('🌐 SSE URL:', sseUrl)
|
||||
|
||||
// Start SSE connection
|
||||
const eventSource = new EventSource(sseUrl)
|
||||
console.log('📡 EventSource oluşturuldu')
|
||||
|
||||
@@ -155,12 +178,10 @@ function ReportGeneration() {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
// Update progress
|
||||
if (data.progress !== undefined) {
|
||||
setProgress(data.progress)
|
||||
}
|
||||
|
||||
// Add log message
|
||||
if (data.message) {
|
||||
const timestamp = new Date().toLocaleTimeString('tr-TR')
|
||||
setLogs(prev => [...prev, {
|
||||
@@ -170,23 +191,17 @@ function ReportGeneration() {
|
||||
}])
|
||||
}
|
||||
|
||||
// Handle completion
|
||||
if (data.type === 'complete') {
|
||||
eventSource.close()
|
||||
|
||||
// Store completion data
|
||||
setCompletionData({
|
||||
report_id: data.report_id,
|
||||
total_products: data.total_products,
|
||||
successful: data.successful
|
||||
})
|
||||
|
||||
// Show completion modal
|
||||
setShowCompletionModal(true)
|
||||
setGenerating(false)
|
||||
}
|
||||
|
||||
// Handle error
|
||||
if (data.type === 'error' && data.progress === -1) {
|
||||
eventSource.close()
|
||||
setGenerating(false)
|
||||
@@ -199,8 +214,6 @@ function ReportGeneration() {
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('❌ SSE Error:', error)
|
||||
console.error('EventSource readyState:', eventSource.readyState)
|
||||
console.error('EventSource url:', eventSource.url)
|
||||
eventSource.close()
|
||||
addLog('Bağlantı hatası oluştu', 'error')
|
||||
setGenerating(false)
|
||||
@@ -214,7 +227,6 @@ function ReportGeneration() {
|
||||
|
||||
const handleViewReport = () => {
|
||||
if (completionData && completionData.report_id) {
|
||||
// Navigate to report dashboard
|
||||
window.location.href = `/reports/${completionData.report_id}`
|
||||
}
|
||||
}
|
||||
@@ -222,7 +234,10 @@ function ReportGeneration() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Yükleniyor...</div>
|
||||
<div className="text-slate-400 flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Yükleniyor...
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -235,35 +250,36 @@ function ReportGeneration() {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Info Card */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 border-l-4 border-l-blue-600">
|
||||
<div className="flex items-start">
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Rapor Oluşturma Süreci</h3>
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>Ana kategoriyi seçin</li>
|
||||
<li>(Opsiyonel) Sadece bir alt kategori seçebilirsiniz</li>
|
||||
<li>"Rapor Oluştur" butonuna tıklayın</li>
|
||||
<li>Sistem seçili kategorilerden verileri çekecek</li>
|
||||
<li>Rapora bir ad verin ve kaydedin</li>
|
||||
<li>"Raporlarım" sekmesinden raporlarınızı görüntüleyin</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const filteredSubCategories = subCategories.filter((subCat) =>
|
||||
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
|
||||
)
|
||||
|
||||
{/* Category Selection */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
1. Kategori Seçin
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Main Category Selection - Horizontal Pills */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-semibold text-slate-500 uppercase tracking-wider">Kategori</h2>
|
||||
{selectedCategory && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!generating) {
|
||||
setSelectedCategory(null)
|
||||
setSubCategories([])
|
||||
setSelectedSubCategories([])
|
||||
setSubBreadcrumb([])
|
||||
}
|
||||
}}
|
||||
disabled={generating}
|
||||
className="text-xs text-slate-400 hover:text-slate-600 transition-colors"
|
||||
>
|
||||
Temizle
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mainCategories.map((category) => (
|
||||
<div
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => {
|
||||
if (!generating) {
|
||||
@@ -271,263 +287,290 @@ function ReportGeneration() {
|
||||
fetchSubCategories(category.id)
|
||||
}
|
||||
}}
|
||||
className={`bg-white border-l-4 ${selectedCategory?.id === category.id ? 'border-blue-600' : 'border-gray-300'} rounded-lg shadow-sm p-4 cursor-pointer hover:shadow-md transition-shadow ${
|
||||
selectedCategory?.id === category.id ? 'ring-2 ring-blue-200' : ''
|
||||
disabled={generating}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||
selectedCategory?.id === category.id
|
||||
? 'bg-orange-500 text-white shadow-sm shadow-orange-200'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-800'
|
||||
} ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900">{category.name}</h3>
|
||||
<p className="text-xs text-gray-600">
|
||||
{category.children_count || 0} alt kategori
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-Category Selection (Optional) */}
|
||||
{selectedCategory && subCategories.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
2. Alt Kategori Seçin (Opsiyonel)
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
İsterseniz sadece bir alt kategori için rapor oluşturabilirsiniz. Seçmezseniz tüm alt kategoriler için rapor oluşturulur.
|
||||
</p>
|
||||
|
||||
{loadingSubCategories ? (
|
||||
<div className="text-center text-gray-500">Alt kategoriler yükleniyor...</div>
|
||||
{/* Sub-Category Selection - Clean List */}
|
||||
{selectedCategory && (subCategories.length > 0 || loadingSubCategories) && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-5 py-4 border-b border-slate-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{subBreadcrumb.length > 0 && (
|
||||
<button
|
||||
onClick={() => !generating && handleBreadcrumbBack(subBreadcrumb.length >= 2 ? subBreadcrumb.length - 2 : -1)}
|
||||
disabled={generating}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-md bg-slate-100 hover:bg-slate-200 text-slate-400 hover:text-slate-600 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<button
|
||||
onClick={() => !generating && subBreadcrumb.length > 0 && handleBreadcrumbBack(-1)}
|
||||
className={`font-medium transition-colors ${subBreadcrumb.length > 0 ? 'text-orange-500 hover:text-orange-600 cursor-pointer' : 'text-slate-800 cursor-default'}`}
|
||||
disabled={generating || subBreadcrumb.length === 0}
|
||||
>
|
||||
{selectedCategory.name}
|
||||
</button>
|
||||
{subBreadcrumb.map((crumb, index) => (
|
||||
<span key={crumb.id} className="flex items-center gap-1.5">
|
||||
<ChevronRight className="w-3 h-3 text-slate-300" />
|
||||
{index < subBreadcrumb.length - 1 ? (
|
||||
<button
|
||||
onClick={() => !generating && handleBreadcrumbBack(index)}
|
||||
className="text-orange-500 hover:text-orange-600 font-medium"
|
||||
disabled={generating}
|
||||
>
|
||||
{crumb.name}
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<span className="text-slate-800 font-medium">{crumb.name}</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowSearch(!showSearch)}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded-md transition-colors ${
|
||||
showSearch ? 'bg-orange-100 text-orange-500' : 'bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600'
|
||||
}`}
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span className="text-xs text-slate-400 tabular-nums">{subCategories.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search - collapsible */}
|
||||
{showSearch && (
|
||||
<div className="relative mt-3">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Alt kategori ara... (örn: Telefon)"
|
||||
placeholder="Ara..."
|
||||
value={subCategorySearch}
|
||||
onChange={(e) => setSubCategorySearch(e.target.value)}
|
||||
disabled={generating}
|
||||
className={`w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:border-blue-600 transition-colors ${
|
||||
generating ? 'opacity-50 cursor-not-allowed bg-gray-100' : 'bg-white'
|
||||
}`}
|
||||
autoFocus
|
||||
className="w-full pl-9 pr-8 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-orange-300 focus:ring-1 focus:ring-orange-300 transition-all"
|
||||
/>
|
||||
{subCategorySearch && (
|
||||
<button
|
||||
onClick={() => setSubCategorySearch('')}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-300 hover:text-slate-500"
|
||||
>
|
||||
×
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selection info and controls */}
|
||||
<div className="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-700">
|
||||
{selectedSubCategories.length === 0
|
||||
? 'Tüm Alt Kategoriler Seçili'
|
||||
: `${selectedSubCategories.length} Kategori Seçildi`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
||||
{/* Selected chips */}
|
||||
{selectedSubCategories.length > 0 && (
|
||||
<div className="px-5 py-3 border-b border-slate-100 bg-orange-50/40">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-[11px] font-semibold text-orange-500 uppercase tracking-wider shrink-0">
|
||||
{selectedSubCategories.length} seçili
|
||||
</span>
|
||||
<div className="w-px h-4 bg-orange-200 shrink-0" />
|
||||
{selectedSubCategories.map((cat) => (
|
||||
<span
|
||||
key={cat.id}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 bg-white rounded-full text-xs font-medium text-slate-700 border border-orange-200 shadow-sm"
|
||||
>
|
||||
{cat.name}
|
||||
<button
|
||||
onClick={() => !generating && setSelectedSubCategories(prev => prev.filter(c => c.id !== cat.id))}
|
||||
disabled={generating}
|
||||
className="text-slate-300 hover:text-red-400 transition-colors ml-0.5"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<button
|
||||
onClick={() => !generating && setSelectedSubCategories([])}
|
||||
disabled={generating}
|
||||
className={`px-3 py-1 text-sm rounded-lg border-2 transition-all ${
|
||||
generating
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'border-red-300 text-red-700 hover:bg-red-50'
|
||||
}`}
|
||||
className="text-[11px] text-slate-400 hover:text-red-400 font-medium transition-colors ml-auto shrink-0"
|
||||
>
|
||||
Seçimi Temizle
|
||||
Temizle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
|
||||
{/* List */}
|
||||
{loadingSubCategories ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-orange-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[420px] overflow-y-auto">
|
||||
{/* Select all row */}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!generating) {
|
||||
const filteredSubs = subCategories.filter((subCat) =>
|
||||
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
|
||||
)
|
||||
setSelectedSubCategories(filteredSubs)
|
||||
if (generating) return
|
||||
const allIds = new Set(selectedSubCategories.map(c => c.id))
|
||||
const allSelected = filteredSubCategories.every(c => allIds.has(c.id))
|
||||
if (allSelected) {
|
||||
const filterIds = new Set(filteredSubCategories.map(c => c.id))
|
||||
setSelectedSubCategories(prev => prev.filter(c => !filterIds.has(c.id)))
|
||||
} else {
|
||||
const newOnes = filteredSubCategories.filter(c => !allIds.has(c.id))
|
||||
setSelectedSubCategories(prev => [...prev, ...newOnes])
|
||||
}
|
||||
}}
|
||||
disabled={generating}
|
||||
className={`px-3 py-1 text-sm rounded-lg border-2 transition-all ${
|
||||
generating
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'border-green-600 text-green-700 hover:bg-green-50'
|
||||
}`}
|
||||
className={`flex items-center px-5 py-2.5 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{subCategorySearch ? 'Gösterilenleri Seç' : 'Tümünü Seç'}
|
||||
</button>
|
||||
<div className={`w-[18px] h-[18px] rounded border-2 flex items-center justify-center mr-3 shrink-0 transition-colors ${
|
||||
filteredSubCategories.length > 0 && filteredSubCategories.every(c => selectedSubCategories.some(s => s.id === c.id))
|
||||
? 'bg-orange-500 border-orange-500'
|
||||
: 'border-slate-300'
|
||||
}`}>
|
||||
{filteredSubCategories.length > 0 && filteredSubCategories.every(c => selectedSubCategories.some(s => s.id === c.id)) && (
|
||||
<Check className="w-3 h-3 text-white" strokeWidth={3} />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Tümünü seç</span>
|
||||
</div>
|
||||
|
||||
{/* Sub-category options */}
|
||||
{(() => {
|
||||
const filteredSubCategories = subCategories.filter((subCat) =>
|
||||
subCat.name.toLowerCase().includes(subCategorySearch.toLowerCase())
|
||||
)
|
||||
|
||||
return filteredSubCategories.length > 0 ? (
|
||||
<div className="max-h-64 overflow-y-auto space-y-2 border rounded-lg p-2">
|
||||
{filteredSubCategories.map((subCat) => {
|
||||
const isSelected = selectedSubCategories.some(cat => cat.id === subCat.id)
|
||||
{filteredSubCategories.length > 0 ? (
|
||||
filteredSubCategories.map((subCat) => {
|
||||
const isSelected = selectedSubCategories.some(c => c.id === subCat.id)
|
||||
const hasChildren = subCat.children_count > 0
|
||||
return (
|
||||
<div
|
||||
key={subCat.id}
|
||||
onClick={() => {
|
||||
if (!generating) {
|
||||
if (isSelected) {
|
||||
// Remove from selection
|
||||
setSelectedSubCategories(prev =>
|
||||
prev.filter(cat => cat.id !== subCat.id)
|
||||
)
|
||||
} else {
|
||||
// Add to selection
|
||||
setSelectedSubCategories(prev => [...prev, subCat])
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={`p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-blue-600 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
className={`group flex items-center px-5 py-3 border-b border-slate-50 transition-colors ${
|
||||
isSelected ? 'bg-orange-50/60' : 'hover:bg-slate-50/80'
|
||||
} ${generating ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => {}} // Handled by parent div onClick
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-600 cursor-pointer"
|
||||
{/* Checkbox */}
|
||||
<button
|
||||
onClick={() => toggleSubCategory(subCat)}
|
||||
disabled={generating}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900">{subCat.name}</span>
|
||||
</div>
|
||||
className="mr-3 shrink-0"
|
||||
>
|
||||
<div className={`w-[18px] h-[18px] rounded border-2 flex items-center justify-center transition-all duration-150 ${
|
||||
isSelected
|
||||
? 'bg-orange-500 border-orange-500 scale-105'
|
||||
: 'border-slate-300 group-hover:border-slate-400'
|
||||
}`}>
|
||||
{isSelected && <Check className="w-3 h-3 text-white" strokeWidth={3} />}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Name - click to select */}
|
||||
<button
|
||||
onClick={() => toggleSubCategory(subCat)}
|
||||
disabled={generating}
|
||||
className="flex-1 text-left min-w-0"
|
||||
>
|
||||
<span className={`text-sm font-medium ${isSelected ? 'text-orange-900' : 'text-slate-700'}`}>
|
||||
{subCat.name}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Children count + drill down */}
|
||||
{hasChildren && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDrillDown(subCat)
|
||||
}}
|
||||
disabled={generating}
|
||||
className="group/drill flex items-center gap-1.5 ml-3 px-2.5 py-1.5 rounded-lg bg-slate-100 text-slate-500 hover:text-orange-600 hover:bg-orange-100 transition-all shrink-0 border border-transparent hover:border-orange-200"
|
||||
title="Alt kategorileri gör"
|
||||
>
|
||||
<span className="text-xs tabular-nums font-medium">{subCat.children_count}</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 transition-transform group-hover/drill:translate-x-0.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500 border rounded-lg bg-gray-50">
|
||||
<p className="font-medium">Sonuç bulunamadı</p>
|
||||
<p className="text-sm mt-1">"{subCategorySearch}" araması için kategori bulunamadı</p>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<p className="text-sm">Sonuç bulunamadı</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate Button */}
|
||||
{selectedCategory && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{subCategories.length > 0 ? '3. Rapor Oluştur' : '2. Rapor Oluştur'}
|
||||
</h2>
|
||||
<div className="flex flex-col items-center justify-center py-4">
|
||||
<p className="text-gray-600 mb-4 text-center">
|
||||
<strong>{selectedCategory.name}</strong> kategorisi için{' '}
|
||||
{/* Generate Button - compact inline */}
|
||||
{selectedCategory && !generating && (
|
||||
<div className="flex items-center justify-between bg-white rounded-xl shadow-sm border border-slate-200 px-5 py-4">
|
||||
<p className="text-sm text-slate-500">
|
||||
<span className="font-semibold text-slate-800">{selectedCategory.name}</span>
|
||||
<span className="mx-1.5 text-slate-300">·</span>
|
||||
{selectedSubCategories.length > 0 ? (
|
||||
<>
|
||||
<strong>{selectedSubCategories.length}</strong> seçili alt kategoriden veri çekilecek
|
||||
</>
|
||||
<span>{selectedSubCategories.length} seçili kategori</span>
|
||||
) : (
|
||||
<>
|
||||
<strong>{selectedCategory.children_count}</strong> alt kategoriden veri çekilecek
|
||||
</>
|
||||
<span>{selectedCategory.children_count || 0} alt kategori</span>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleGenerateReport}
|
||||
disabled={generating}
|
||||
className={`px-8 py-4 rounded-lg font-medium text-lg transition-all ${
|
||||
generating
|
||||
? 'bg-gray-300 text-gray-600 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md hover:shadow-lg'
|
||||
}`}
|
||||
className="px-6 py-2.5 bg-orange-500 text-white text-sm rounded-lg font-medium hover:bg-orange-600 shadow-sm hover:shadow transition-all"
|
||||
>
|
||||
{generating ? (
|
||||
<span className="flex items-center">
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-600"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
Rapor Oluşturuluyor... {progress}%
|
||||
</span>
|
||||
) : (
|
||||
<>Rapor Oluştur</>
|
||||
)}
|
||||
Rapor Oluştur
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Bar */}
|
||||
{/* Progress - shows during generation */}
|
||||
{generating && (
|
||||
<div className="w-full max-w-md mt-6">
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 px-5 py-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-slate-700">Rapor oluşturuluyor...</span>
|
||||
<span className="text-sm font-semibold text-orange-500 tabular-nums">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-3 rounded-full transition-all duration-500"
|
||||
className="bg-orange-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terminal Logs */}
|
||||
{logs.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<span>Terminal - Rapor Oluşturuluyor</span>
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
|
||||
<span className="text-sm text-gray-600">Canlı</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-4 max-h-96 overflow-y-auto border-2 border-gray-700">
|
||||
<div className="bg-slate-900 rounded-xl overflow-hidden border border-slate-700">
|
||||
{/* Terminal Header */}
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-700">
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-slate-700/50">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-red-500/80"></div>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/80"></div>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-green-500/80"></div>
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs font-mono ml-2">trendyol-analytics-terminal</span>
|
||||
<span className="text-slate-500 text-xs font-mono ml-2">terminal</span>
|
||||
{generating && <div className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse ml-auto"></div>}
|
||||
</div>
|
||||
|
||||
{/* Terminal Content */}
|
||||
<div className="p-4 max-h-80 overflow-y-auto">
|
||||
{logs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`font-mono text-sm mb-2 ${
|
||||
className={`font-mono text-xs mb-1.5 ${
|
||||
log.type === 'error'
|
||||
? 'text-red-400'
|
||||
: log.type === 'success'
|
||||
@@ -538,14 +581,12 @@ function ReportGeneration() {
|
||||
? 'text-blue-400'
|
||||
: log.type === 'processing'
|
||||
? 'text-cyan-400'
|
||||
: 'text-gray-300'
|
||||
: 'text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<span className="text-gray-500">[{log.timestamp}]</span> {log.message}
|
||||
<span className="text-slate-600">[{log.timestamp}]</span> {log.message}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Auto-scroll anchor */}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -553,74 +594,68 @@ function ReportGeneration() {
|
||||
|
||||
{/* Name Modal */}
|
||||
{showNameModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-8 max-w-md w-full mx-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Rapora Ad Verin</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Rapor başarıyla oluşturuldu! Kaydetmek için bir isim verin.
|
||||
</p>
|
||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl">
|
||||
<h2 className="text-lg font-bold text-slate-900 mb-1">Rapor Adı</h2>
|
||||
<p className="text-sm text-slate-400 mb-4">Raporunuz için bir isim belirleyin</p>
|
||||
<input
|
||||
type="text"
|
||||
value={reportName}
|
||||
onChange={(e) => setReportName(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-6"
|
||||
placeholder="Örn: Kasım Ayı Kozmetik Raporu"
|
||||
className="w-full px-4 py-2.5 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent mb-4"
|
||||
placeholder="Örn: Kasım Kozmetik Raporu"
|
||||
autoFocus
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleStartReportGeneration()}
|
||||
/>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowNameModal(false)
|
||||
setGenerating(false)
|
||||
}}
|
||||
className="flex-1 px-4 py-3 bg-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-400 transition-colors"
|
||||
className="flex-1 px-4 py-2.5 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStartReportGeneration}
|
||||
className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
className="flex-1 px-4 py-2.5 bg-orange-500 text-white rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
Rapor Oluştur
|
||||
Başla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completion Modal - Shows when report is 100% complete */}
|
||||
{/* Completion Modal */}
|
||||
{showCompletionModal && completionData && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl p-8 max-w-md w-full shadow-2xl">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-12 h-12 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl p-6 max-w-sm w-full shadow-2xl text-center">
|
||||
<div className="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Check className="w-7 h-7 text-green-600" strokeWidth={2.5} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">Rapor Tamamlandı!</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
<strong>{reportName}</strong> başarıyla oluşturuldu.
|
||||
</p>
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-6 border border-gray-200">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-600">Toplam Ürün:</span>
|
||||
<span className="font-bold text-gray-900">{completionData.total_products}</span>
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-1">Tamamlandı</h3>
|
||||
<p className="text-sm text-slate-400 mb-5">{reportName}</p>
|
||||
<div className="flex gap-4 justify-center mb-5 text-center">
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-slate-900">{completionData.total_products}</p>
|
||||
<p className="text-xs text-slate-400">Ürün</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600">Başarılı Kategori:</span>
|
||||
<span className="font-bold text-green-600">{completionData.successful}</span>
|
||||
<div className="w-px bg-slate-200" />
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-green-600">{completionData.successful}</p>
|
||||
<p className="text-xs text-slate-400">Kategori</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleViewReport}
|
||||
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
className="w-full px-6 py-2.5 bg-orange-500 text-white rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
Raporu Görüntüle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { API_URL, fetchWithTimeout } from '../config/api'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG, POLLING_CONFIG, calculateNextDelay } from '../config/api'
|
||||
import { FileBarChart, Trash2, Eye, Calendar, Layers, Package, Sparkles, GitCompareArrows } from 'lucide-react'
|
||||
|
||||
function ReportList() {
|
||||
const navigate = useNavigate()
|
||||
const [reports, setReports] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [enrichingReports, setEnrichingReports] = useState({}) // { reportId: { status, progress, enriched } }
|
||||
|
||||
useEffect(() => {
|
||||
fetchReports()
|
||||
@@ -19,6 +22,16 @@ function ReportList() {
|
||||
if (!response.ok) throw new Error('Failed to fetch reports')
|
||||
const data = await response.json()
|
||||
setReports(data)
|
||||
|
||||
// Check enrichment status for each report
|
||||
data.forEach(report => {
|
||||
if (report.is_enriched) {
|
||||
setEnrichingReports(prev => ({
|
||||
...prev,
|
||||
[report.id]: { status: 'completed', progress: 100, enriched: true }
|
||||
}))
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
@@ -46,6 +59,74 @@ function ReportList() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnrich = async (reportId) => {
|
||||
// Set loading state
|
||||
setEnrichingReports(prev => ({
|
||||
...prev,
|
||||
[reportId]: { status: 'loading', progress: 0, enriched: false }
|
||||
}))
|
||||
|
||||
try {
|
||||
// Start enrichment
|
||||
const response = await fetchWithTimeout(
|
||||
`${API_URL}/api/reports/${reportId}/enrich/start`,
|
||||
{ method: 'POST' },
|
||||
TIMEOUT_CONFIG.ENRICHMENT
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}))
|
||||
throw new Error(errData.detail || 'Zenginleştirme başlatılamadı')
|
||||
}
|
||||
|
||||
// Start polling for status
|
||||
pollEnrichmentStatus(reportId)
|
||||
} catch (err) {
|
||||
setEnrichingReports(prev => ({
|
||||
...prev,
|
||||
[reportId]: { status: 'error', progress: 0, enriched: false, error: err.message }
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const pollEnrichmentStatus = async (reportId) => {
|
||||
let delay = POLLING_CONFIG.INITIAL_DELAY
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await fetchWithTimeout(
|
||||
`${API_URL}/api/reports/${reportId}/enrich/status`
|
||||
)
|
||||
|
||||
if (!response.ok) throw new Error('Status check failed')
|
||||
const data = await response.json()
|
||||
|
||||
const progress = data.progress || 0
|
||||
const isComplete = data.status === 'completed' || progress >= 100
|
||||
|
||||
setEnrichingReports(prev => ({
|
||||
...prev,
|
||||
[reportId]: {
|
||||
status: isComplete ? 'completed' : 'loading',
|
||||
progress,
|
||||
enriched: isComplete
|
||||
}
|
||||
}))
|
||||
|
||||
if (!isComplete) {
|
||||
delay = calculateNextDelay(delay)
|
||||
setTimeout(poll, delay)
|
||||
}
|
||||
} catch {
|
||||
// On error, retry a few times
|
||||
delay = calculateNextDelay(delay)
|
||||
setTimeout(poll, delay)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(poll, delay)
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('tr-TR', {
|
||||
@@ -60,14 +141,14 @@ function ReportList() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Yükleniyor...</div>
|
||||
<div className="text-slate-400">Yükleniyor...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p className="text-red-700">Hata: {error}</p>
|
||||
</div>
|
||||
)
|
||||
@@ -76,8 +157,8 @@ function ReportList() {
|
||||
if (reports.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<h3 className="text-xl font-semibold text-gray-800 mb-2">Henüz Rapor Yok</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
<h3 className="text-xl font-semibold text-slate-800 mb-2">Henüz Rapor Yok</h3>
|
||||
<p className="text-slate-500 mb-6">
|
||||
"Rapor Oluştur" sekmesinden ilk raporunuzu oluşturun
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,70 +168,123 @@ function ReportList() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Raporlarım</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Raporlarım</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Toplam {reports.length} rapor
|
||||
</p>
|
||||
</div>
|
||||
{reports.length >= 2 && (
|
||||
<button
|
||||
onClick={() => navigate('/compare')}
|
||||
className="flex items-center gap-2 px-4 py-2 text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors text-sm font-medium"
|
||||
>
|
||||
<GitCompareArrows size={16} />
|
||||
Karşılaştır
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reports Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{reports.map((report) => (
|
||||
{reports.map((report) => {
|
||||
const enrichState = enrichingReports[report.id]
|
||||
const isEnriched = enrichState?.enriched || report.is_enriched
|
||||
const isEnriching = enrichState?.status === 'loading'
|
||||
const enrichProgress = enrichState?.progress || 0
|
||||
|
||||
return (
|
||||
<div
|
||||
key={report.id}
|
||||
className="bg-white rounded-lg shadow-sm border border-gray-200 border-l-4 border-l-blue-600 p-6 hover:shadow-md transition-shadow"
|
||||
className="bg-white rounded-xl shadow-sm border border-slate-200 border-l-4 border-l-orange-500 p-6 hover:shadow-md transition-shadow"
|
||||
>
|
||||
{/* Report Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 text-base">
|
||||
<h3 className="font-semibold text-slate-900 text-base">
|
||||
{report.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
{report.category_name}
|
||||
</p>
|
||||
</div>
|
||||
{isEnriched && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-100 text-emerald-700 text-xs font-medium rounded-full">
|
||||
<Sparkles size={10} />
|
||||
Zengin
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
|
||||
<p className="text-xs text-gray-600 font-medium mb-1">Alt Kategori</p>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
<div className="bg-blue-50 rounded-xl p-3 border border-blue-100">
|
||||
<p className="text-xs text-slate-500 font-medium mb-1">Alt Kategori</p>
|
||||
<p className="text-lg font-bold text-slate-900">
|
||||
{report.total_subcategories}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-3 border border-green-100">
|
||||
<p className="text-xs text-gray-600 font-medium mb-1">Ürün</p>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
<div className="bg-green-50 rounded-xl p-3 border border-green-100">
|
||||
<p className="text-xs text-slate-500 font-medium mb-1">Ürün</p>
|
||||
<p className="text-lg font-bold text-slate-900">
|
||||
{report.total_products.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enrichment Progress */}
|
||||
{isEnriching && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-amber-600 font-medium">Zenginleştiriliyor...</span>
|
||||
<span className="text-xs text-amber-600 font-medium tabular-nums">{enrichProgress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-amber-100 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-amber-500 h-1.5 rounded-full transition-all duration-500"
|
||||
style={{ width: `${enrichProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<div className="text-xs text-gray-500 mb-4">
|
||||
<div className="text-xs text-slate-400 mb-4">
|
||||
{formatDate(report.created_at)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
to={`/reports/${report.id}`}
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors text-sm text-center"
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-orange-500 text-white rounded-xl font-medium hover:bg-orange-600 transition-colors text-sm text-center"
|
||||
>
|
||||
<Eye size={16} />
|
||||
Görüntüle
|
||||
</Link>
|
||||
{!isEnriched && !isEnriching && (
|
||||
<button
|
||||
onClick={() => handleEnrich(report.id)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-50 border border-amber-200 text-amber-700 rounded-xl font-medium hover:bg-amber-100 transition-colors text-sm"
|
||||
title="Sosyal kanıt verilerini zenginleştir"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
Zenginleştir
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDeleteReport(report.id, report.name)}
|
||||
className="px-4 py-2 bg-red-500 text-white rounded-lg font-medium hover:bg-red-600 transition-colors text-sm"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 bg-white border border-red-300 text-red-600 rounded-xl font-medium hover:bg-red-50 transition-colors text-sm"
|
||||
>
|
||||
Sil
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ScatterChart, Scatter } from 'recharts'
|
||||
import KpiCard from '../ui/KpiCard'
|
||||
import { Barcode, Globe, Flag, Award } from 'lucide-react'
|
||||
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
|
||||
|
||||
export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null })
|
||||
@@ -60,7 +63,7 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
if (!barcodeAnalytics) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Barkod analizi yükleniyor...</p>
|
||||
<p className="text-slate-400">Barkod analizi yükleniyor...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -70,100 +73,58 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
{/* Row 1: KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Total Products with Barcode */}
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm font-medium">Barkodlu Ürün</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{barcodeAnalytics.kpis.totalWithBarcode.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Barkodlu Ürün"
|
||||
value={barcodeAnalytics.kpis.totalWithBarcode.toLocaleString('tr-TR')}
|
||||
icon={Barcode}
|
||||
color="blue"
|
||||
/>
|
||||
|
||||
{/* Total Countries */}
|
||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100 text-sm font-medium">Tespit Edilen Ülke</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{barcodeAnalytics.kpis.totalCountries}
|
||||
</p>
|
||||
{barcodeAnalytics.kpis.undetectedProducts > 0 && (
|
||||
<p className="text-green-100 text-xs mt-1">
|
||||
{barcodeAnalytics.kpis.undetectedProducts.toLocaleString('tr-TR')} tespit edilemedi
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Tespit Edilen Ülke"
|
||||
value={barcodeAnalytics.kpis.totalCountries}
|
||||
subtitle={barcodeAnalytics.kpis.undetectedProducts > 0 ? `${barcodeAnalytics.kpis.undetectedProducts.toLocaleString('tr-TR')} tespit edilemedi` : undefined}
|
||||
icon={Globe}
|
||||
color="emerald"
|
||||
/>
|
||||
|
||||
{/* Domestic Share */}
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm font-medium">Yerli Ürün Payı</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{barcodeAnalytics.kpis.domesticShare}%
|
||||
</p>
|
||||
<p className="text-purple-100 text-xs mt-1">Türkiye menşeili</p>
|
||||
</div>
|
||||
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Yerli Ürün Payı"
|
||||
value={`${barcodeAnalytics.kpis.domesticShare}%`}
|
||||
subtitle="Türkiye menşeili"
|
||||
icon={Flag}
|
||||
color="violet"
|
||||
/>
|
||||
|
||||
{/* Top Country */}
|
||||
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100 text-sm font-medium">En Çok Ürün</p>
|
||||
<p className="text-2xl font-bold mt-2">
|
||||
{barcodeAnalytics.kpis.topCountry}
|
||||
</p>
|
||||
<p className="text-orange-100 text-xs mt-1">
|
||||
{barcodeAnalytics.kpis.topCountryShare.toFixed(1)}% pay
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="En Çok Ürün"
|
||||
value={barcodeAnalytics.kpis.topCountry}
|
||||
subtitle={`${barcodeAnalytics.kpis.topCountryShare.toFixed(1)}% pay`}
|
||||
icon={Award}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Category-Country Heatmap - FULL WIDTH */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kategori-Ülke Isı Haritası (Top 10x10)</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">Hangi ülkelerin hangi kategorilerde güçlü olduğunu gösterir. Koyu renkler daha yüksek satış hacmini temsil eder.</p>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Kategori-Ülke Isı Haritası (Top 10x10)</h3>
|
||||
<p className="text-xs text-slate-400 mb-4">Hangi ülkelerin hangi kategorilerde güçlü olduğunu gösterir. Koyu renkler daha yüksek satış hacmini temsil eder.</p>
|
||||
|
||||
{barcodeAnalytics.categoryCountryMatrix && barcodeAnalytics.categoryCountryMatrix.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-200 px-2 py-2 bg-gray-50 text-xs font-medium text-gray-700 sticky left-0 z-10">
|
||||
<th className="border border-slate-200 px-2 py-2 bg-slate-50 text-xs font-medium text-slate-700 sticky left-0 z-10">
|
||||
Kategori / Ülke
|
||||
</th>
|
||||
{barcodeAnalytics.topCountriesForHeatmap.map((country) => (
|
||||
<th
|
||||
key={country.name}
|
||||
className="border border-gray-200 px-2 py-2 bg-gray-50 text-xs font-medium text-gray-700 whitespace-nowrap"
|
||||
className="border border-slate-200 px-2 py-2 bg-slate-50 text-xs font-medium text-slate-700 whitespace-nowrap"
|
||||
style={{ minWidth: '80px' }}
|
||||
>
|
||||
{country.name}
|
||||
@@ -174,7 +135,7 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
<tbody>
|
||||
{barcodeAnalytics.topCategories.map((category) => (
|
||||
<tr key={category}>
|
||||
<td className="border border-gray-200 px-2 py-2 text-xs font-medium text-gray-900 bg-gray-50 sticky left-0 z-10 whitespace-nowrap">
|
||||
<td className="border border-slate-200 px-2 py-2 text-xs font-medium text-slate-900 bg-slate-50 sticky left-0 z-10 whitespace-nowrap">
|
||||
{category}
|
||||
</td>
|
||||
{barcodeAnalytics.topCountriesForHeatmap.map((country) => {
|
||||
@@ -184,8 +145,8 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
|
||||
if (!cell) {
|
||||
return (
|
||||
<td key={country.name} className="border border-gray-200 px-2 py-2 bg-gray-50">
|
||||
<div className="text-center text-xs text-gray-400">-</div>
|
||||
<td key={country.name} className="border border-slate-200 px-2 py-2 bg-slate-50">
|
||||
<div className="text-center text-xs text-slate-400">-</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
@@ -197,12 +158,12 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
if (intensity > 0.7) return 'bg-blue-600 text-white'
|
||||
if (intensity > 0.5) return 'bg-blue-500 text-white'
|
||||
if (intensity > 0.3) return 'bg-blue-400 text-white'
|
||||
if (intensity > 0.15) return 'bg-blue-300 text-gray-900'
|
||||
return 'bg-blue-100 text-gray-900'
|
||||
if (intensity > 0.15) return 'bg-blue-300 text-slate-900'
|
||||
return 'bg-blue-100 text-slate-900'
|
||||
}
|
||||
|
||||
return (
|
||||
<td key={country.name} className="border border-gray-200 p-0">
|
||||
<td key={country.name} className="border border-slate-200 p-0">
|
||||
<div
|
||||
className={`px-2 py-2 ${getColor(intensity)} text-center cursor-help transition-all hover:scale-105`}
|
||||
title={`${category} - ${country.name}\nSatış: ${cell.orders.toLocaleString('tr-TR')}\nCiro: ₺${cell.revenue.toLocaleString('tr-TR')}\nÜrün: ${cell.productCount}\nOrt: ${cell.avgOrdersPerProduct} satış/ürün`}
|
||||
@@ -221,45 +182,45 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-4 flex items-center justify-end gap-2 text-xs">
|
||||
<span className="text-gray-600">Düşük</span>
|
||||
<span className="text-slate-500">Düşük</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-6 h-4 bg-blue-100 border border-gray-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-300 border border-gray-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-400 border border-gray-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-500 border border-gray-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-600 border border-gray-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-100 border border-slate-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-300 border border-slate-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-400 border border-slate-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-500 border border-slate-300"></div>
|
||||
<div className="w-6 h-4 bg-blue-600 border border-slate-300"></div>
|
||||
</div>
|
||||
<span className="text-gray-600">Yüksek</span>
|
||||
<span className="text-slate-500">Yüksek</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
|
||||
<p className="text-gray-500 text-sm">Veri yükleniyor...</p>
|
||||
<div className="flex items-center justify-center h-64 bg-slate-50 rounded-xl border-2 border-dashed border-slate-300">
|
||||
<p className="text-slate-400 text-sm">Veri yükleniyor...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Top 20 Countries - FULL WIDTH with 10x10 Grid */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4">
|
||||
En Çok Satan Ülkeler (Top 20)
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Left Column: Countries 1-10 */}
|
||||
<div className="space-y-2">
|
||||
{barcodeAnalytics.topByOrders.slice(0, 10).map((country, index) => (
|
||||
<div key={country.name} className="flex items-center justify-between p-3 bg-gradient-to-r from-blue-50 to-transparent rounded-lg hover:from-blue-100 transition-colors">
|
||||
<div key={country.name} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-orange-50/30 transition-colors">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-blue-500 text-white rounded-full font-bold text-sm">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-orange-500 text-white rounded-full font-bold text-sm">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-800">{country.name}</span>
|
||||
<span className="font-semibold text-slate-800">{country.name}</span>
|
||||
</div>
|
||||
<div className="text-right space-y-1">
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Satış:</span> {country.totalOrders.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Ciro:</span> ₺{country.totalRevenue.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -270,18 +231,18 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
{/* Right Column: Countries 11-20 */}
|
||||
<div className="space-y-2">
|
||||
{barcodeAnalytics.topByOrders.slice(10, 20).map((country, index) => (
|
||||
<div key={country.name} className="flex items-center justify-between p-3 bg-gradient-to-r from-purple-50 to-transparent rounded-lg hover:from-purple-100 transition-colors">
|
||||
<div key={country.name} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-orange-50/30 transition-colors">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-purple-500 text-white rounded-full font-bold text-sm">
|
||||
{index + 11}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-800">{country.name}</span>
|
||||
<span className="font-semibold text-slate-800">{country.name}</span>
|
||||
</div>
|
||||
<div className="text-right space-y-1">
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Satış:</span> {country.totalOrders.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Ciro:</span> ₺{country.totalRevenue.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -294,8 +255,8 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
{/* Row 4: Charts - 50/50 Split */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Country Sales Bar Chart (Top 15) */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ülke Bazlı Satış (Top 15)</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Ülke Bazlı Satış (Top 15)</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={barcodeAnalytics.topByOrders.slice(0, 15)}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
@@ -309,16 +270,16 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<Tooltip
|
||||
formatter={(value) => value.toLocaleString('tr-TR')}
|
||||
contentStyle={{ fontSize: '12px' }}
|
||||
{...CHART_TOOLTIP_STYLE}
|
||||
/>
|
||||
<Bar dataKey="totalOrders" fill="#3b82f6" name="Satış Adedi" />
|
||||
<Bar dataKey="totalOrders" fill={CHART_COLORS[0]} name="Satış Adedi" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Price vs Revenue Scatter Chart */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ortalama Fiyat / Ciro İlişkisi</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Ortalama Fiyat / Ciro İlişkisi</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ScatterChart>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
@@ -341,12 +302,12 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3">
|
||||
<p className="font-semibold text-gray-900 mb-2 text-sm">{data.name}</p>
|
||||
<p className="font-semibold text-gray-900 text-sm">
|
||||
<div className="bg-slate-800 border-none rounded-xl shadow-lg p-3">
|
||||
<p className="font-semibold text-slate-50 mb-2 text-sm">{data.name}</p>
|
||||
<p className="font-semibold text-slate-200 text-sm">
|
||||
Ciro: ₺{data.totalRevenue.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="font-semibold text-gray-900 text-sm">
|
||||
<p className="font-semibold text-slate-200 text-sm">
|
||||
Satış: {data.totalOrders.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -357,91 +318,91 @@ export default function BarcodeTab({ barcodeAnalytics }) {
|
||||
/>
|
||||
<Scatter
|
||||
data={barcodeAnalytics.countries.filter(c => c.totalRevenue > 0)}
|
||||
fill="#8b5cf6"
|
||||
fill={CHART_COLORS[3]}
|
||||
name="Ülkeler"
|
||||
/>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="text-xs text-gray-500 mt-2 text-center">
|
||||
<p className="text-xs text-slate-400 mt-2 text-center">
|
||||
Her nokta bir ülkeyi temsil eder (Ortalama fiyat vs Toplam ciro)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 5: Detailed Country Comparison Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Ülke Karşılaştırması</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Ülke Karşılaştırması</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="border-b-2 border-gray-200 bg-gray-50">
|
||||
<thead className="border-b-2 border-slate-200 bg-slate-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-4 py-3 text-left text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-left text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
Ülke{renderSortIndicator('name')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('productCount')}
|
||||
>
|
||||
Ürün{renderSortIndicator('productCount')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('totalOrders')}
|
||||
>
|
||||
Satış{renderSortIndicator('totalOrders')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('totalRevenue')}
|
||||
>
|
||||
Ciro{renderSortIndicator('totalRevenue')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('marketShare')}
|
||||
>
|
||||
Pay %{renderSortIndicator('marketShare')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('avgPrice')}
|
||||
>
|
||||
Ort. Fiyat{renderSortIndicator('avgPrice')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('categoryCount')}
|
||||
>
|
||||
Kategori{renderSortIndicator('categoryCount')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('brandCount')}
|
||||
>
|
||||
Marka{renderSortIndicator('brandCount')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{getSortedData().map((country) => (
|
||||
<tr key={country.name} className="hover:bg-gray-50">
|
||||
<tr key={country.name} className="hover:bg-orange-50/30">
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm font-semibold text-gray-900">{country.name}</div>
|
||||
<div className="text-sm font-semibold text-slate-900">{country.name}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">{country.productCount}</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">{country.totalOrders.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">₺{country.totalRevenue.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">{country.productCount}</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">{country.totalOrders.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">₺{country.totalRevenue.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-600">
|
||||
{country.marketShare.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">₺{country.avgPrice.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-gray-900">{country.categoryCount}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-gray-900">{country.brandCount}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">₺{country.avgPrice.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-slate-900">{country.categoryCount}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-slate-900">{country.brandCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, ScatterChart, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, BarChart, Bar } from 'recharts'
|
||||
import KpiCard from '../ui/KpiCard'
|
||||
import { Tag, Trophy, Package, PieChart as PieChartIcon } from 'lucide-react'
|
||||
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
|
||||
|
||||
export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort, brandSortConfig }) {
|
||||
// Pagination state
|
||||
@@ -23,7 +26,7 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
if (!brandAnalytics) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Brand analizi yükleniyor...</p>
|
||||
<p className="text-slate-400">Brand analizi yükleniyor...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -32,113 +35,73 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
<div className="space-y-6">
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm font-medium">Toplam Marka</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{brandAnalytics.kpis.totalBrands.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-blue-100 text-xs mt-1">Benzersiz marka sayısı</p>
|
||||
</div>
|
||||
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100 text-sm font-medium">Lider Marka Payı</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
%{brandAnalytics.kpis.leaderShare}
|
||||
</p>
|
||||
<p className="text-green-100 text-xs mt-1">{brandAnalytics.topByOrders[0]?.name}</p>
|
||||
</div>
|
||||
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm font-medium">Ort. Marka Başına Ürün</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{brandAnalytics.kpis.avgProductsPerBrand}
|
||||
</p>
|
||||
<p className="text-purple-100 text-xs mt-1">Ürün çeşitliliği</p>
|
||||
</div>
|
||||
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100 text-sm font-medium">Pazar Yoğunlaşması</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{brandAnalytics.kpis.hhi}
|
||||
</p>
|
||||
<p className="text-orange-100 text-xs mt-1">{brandAnalytics.kpis.marketConcentration} (HHI)</p>
|
||||
</div>
|
||||
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Toplam Marka"
|
||||
value={brandAnalytics.kpis.totalBrands.toLocaleString('tr-TR')}
|
||||
subtitle="Benzersiz marka sayısı"
|
||||
icon={Tag}
|
||||
color="blue"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Lider Marka Payı"
|
||||
value={`%${brandAnalytics.kpis.leaderShare}`}
|
||||
subtitle={brandAnalytics.topByOrders[0]?.name}
|
||||
icon={Trophy}
|
||||
color="emerald"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Ort. Marka Başına Ürün"
|
||||
value={brandAnalytics.kpis.avgProductsPerBrand}
|
||||
subtitle="Ürün çeşitliliği"
|
||||
icon={Package}
|
||||
color="violet"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Pazar Yoğunlaşması"
|
||||
value={brandAnalytics.kpis.hhi}
|
||||
subtitle={`${brandAnalytics.kpis.marketConcentration} (HHI)`}
|
||||
icon={PieChartIcon}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Top 20 Brands Full-Width Two-Column Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satan Markalar (Top 20)</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satan Markalar (Top 20)</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column - Brands 1-10 */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">#</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Marka</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Satış</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Ciro</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Pay %</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">Marka</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Satış</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Ciro</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Pay %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{brandAnalytics.topByOrders.slice(0, 10).map((brand, index) => {
|
||||
const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1)
|
||||
return (
|
||||
<tr key={brand.name} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2 text-sm font-medium text-gray-900">{index + 1}</td>
|
||||
<tr key={brand.name} className="hover:bg-orange-50/30 even:bg-slate-50/50">
|
||||
<td className="px-3 py-2 text-sm font-medium text-slate-900">{index + 1}</td>
|
||||
<td className="px-3 py-2">
|
||||
<a
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
|
||||
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
|
||||
>
|
||||
{brand.name}
|
||||
</a>
|
||||
<p className="text-xs text-gray-500">{brand.productCount} ürün</p>
|
||||
<p className="text-xs text-slate-400">{brand.productCount} ürün</p>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm text-gray-900">
|
||||
<td className="px-3 py-2 text-right text-sm text-slate-900">
|
||||
{brand.totalOrders.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm text-gray-900">
|
||||
<td className="px-3 py-2 text-right text-sm text-slate-900">
|
||||
₺{Math.round(brand.totalRevenue).toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm font-semibold text-green-600">
|
||||
@@ -154,36 +117,36 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
{/* Right Column - Brands 11-20 */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">#</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Marka</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Satış</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Ciro</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">Pay %</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">Marka</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Satış</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Ciro</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-slate-400">Pay %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{brandAnalytics.topByOrders.slice(10, 20).map((brand, index) => {
|
||||
const marketShare = ((brand.totalOrders / brandAnalytics.totalOrders) * 100).toFixed(1)
|
||||
return (
|
||||
<tr key={brand.name} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2 text-sm font-medium text-gray-900">{index + 11}</td>
|
||||
<tr key={brand.name} className="hover:bg-orange-50/30 even:bg-slate-50/50">
|
||||
<td className="px-3 py-2 text-sm font-medium text-slate-900">{index + 11}</td>
|
||||
<td className="px-3 py-2">
|
||||
<a
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
|
||||
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
|
||||
>
|
||||
{brand.name}
|
||||
</a>
|
||||
<p className="text-xs text-gray-500">{brand.productCount} ürün</p>
|
||||
<p className="text-xs text-slate-400">{brand.productCount} ürün</p>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm text-gray-900">
|
||||
<td className="px-3 py-2 text-right text-sm text-slate-900">
|
||||
{brand.totalOrders.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm text-gray-900">
|
||||
<td className="px-3 py-2 text-right text-sm text-slate-900">
|
||||
₺{Math.round(brand.totalRevenue).toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-sm font-semibold text-green-600">
|
||||
@@ -201,8 +164,8 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
{/* Row 3: Market Share Chart & Price/Performance Matrix */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Market Share Pie Chart */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Pazar Payı Dağılımı</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Pazar Payı Dağılımı</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
@@ -221,23 +184,23 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#6b7280'].map((color, index) => (
|
||||
{CHART_COLORS.slice(0, 6).map((color, index) => (
|
||||
<Cell key={`cell-${index}`} fill={color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} />
|
||||
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} {...CHART_TOOLTIP_STYLE} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded">
|
||||
<p className="text-sm text-gray-700">
|
||||
Top 3 marka pazarın <span className="font-bold text-blue-600">%{brandAnalytics.kpis.top3Share}</span>'ini kontrol ediyor
|
||||
<div className="mt-4 p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-sm text-slate-700">
|
||||
Top 3 marka pazarın <span className="font-bold text-orange-500">%{brandAnalytics.kpis.top3Share}</span>'ini kontrol ediyor
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price/Quality Scatter Plot */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat/Performans Matrisi</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat/Performans Matrisi</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
@@ -260,7 +223,7 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white p-3 border border-gray-200 rounded shadow-lg">
|
||||
<div className="bg-white p-3 border border-slate-200 rounded-lg shadow-lg">
|
||||
<p className="font-semibold">{data.name}</p>
|
||||
<p className="text-sm">Fiyat: ₺{data.avgPrice.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-sm">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
|
||||
@@ -273,32 +236,32 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
/>
|
||||
<Scatter
|
||||
data={brandAnalytics.topByOrders.slice(0, 15)}
|
||||
fill="#3b82f6"
|
||||
fill={CHART_COLORS[0]}
|
||||
opacity={0.6}
|
||||
/>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="text-xs text-gray-500 mt-2 text-center">
|
||||
<p className="text-xs text-slate-400 mt-2 text-center">
|
||||
Kabarcık boyutu: Sosyal kanıt skoru (görüntülenme + satış + ürün çeşitliliği)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Detailed Brand Comparison Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Marka Karşılaştırması</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Marka Karşılaştırması</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th
|
||||
onClick={() => handleBrandSort('name')}
|
||||
className="px-4 py-3 text-left text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-left text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Marka
|
||||
{brandSortConfig.key === 'name' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{brandSortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -306,12 +269,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleBrandSort('totalOrders')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Satış
|
||||
{brandSortConfig.key === 'totalOrders' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{brandSortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -319,12 +282,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleBrandSort('totalRevenue')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Ciro
|
||||
{brandSortConfig.key === 'totalRevenue' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{brandSortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -332,12 +295,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleBrandSort('productCount')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Ürün Sayısı
|
||||
{brandSortConfig.key === 'productCount' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{brandSortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -345,12 +308,12 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleBrandSort('categoryCount')}
|
||||
className="px-4 py-3 text-center text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-center text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
Kategori Çeşitliliği
|
||||
{brandSortConfig.key === 'categoryCount' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{brandSortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -358,41 +321,41 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleBrandSort('avgPrice')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-slate-100 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Ort. Fiyat
|
||||
{brandSortConfig.key === 'avgPrice' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{brandSortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500">Fiyat Aralığı</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Segment</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">Fiyat Aralığı</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-400">Segment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{paginatedBrands.map(brand => (
|
||||
<tr key={brand.name} className="hover:bg-gray-50">
|
||||
<tr key={brand.name} className="hover:bg-orange-50/30 even:bg-slate-50/50">
|
||||
<td className="px-4 py-3">
|
||||
<a
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
|
||||
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
|
||||
>
|
||||
{brand.name}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">
|
||||
{brand.totalOrders.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600">
|
||||
₺{brand.totalRevenue.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">
|
||||
{brand.productCount}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
@@ -401,10 +364,10 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
<span className="text-xs text-indigo-600">kategori</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">
|
||||
₺{brand.avgPrice.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-xs text-gray-600">
|
||||
<td className="px-4 py-3 text-right text-xs text-slate-500">
|
||||
{brand.minPrice !== brand.maxPrice
|
||||
? `₺${brand.minPrice.toLocaleString('tr-TR')} - ₺${brand.maxPrice.toLocaleString('tr-TR')}`
|
||||
: `₺${brand.minPrice.toLocaleString('tr-TR')}`
|
||||
@@ -429,23 +392,23 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-slate-500">
|
||||
Sayfa {currentPage} / {totalPages} (Toplam {sortedBrands.length.toLocaleString('tr-TR')} marka)
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
««
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
‹ Önceki
|
||||
</button>
|
||||
@@ -470,8 +433,8 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`px-3 py-1.5 text-sm border rounded-xl transition-colors ${
|
||||
currentPage === pageNum
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'border-gray-300 hover:bg-gray-50'
|
||||
? 'bg-orange-500 text-white border-orange-500'
|
||||
: 'border-slate-300 hover:bg-orange-50/30'
|
||||
}`}
|
||||
>
|
||||
{pageNum}
|
||||
@@ -483,14 +446,14 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Sonraki ›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
»»
|
||||
</button>
|
||||
@@ -501,19 +464,19 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
</div>
|
||||
|
||||
{/* Row 5: Market Insights */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">
|
||||
Pazar İçgörüleri
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Lider Marka Dominansı</p>
|
||||
<p className="text-lg font-bold text-blue-600 mt-1">
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Lider Marka Dominansı</p>
|
||||
<p className="text-lg font-bold text-orange-500 mt-1">
|
||||
{brandAnalytics.topByOrders[0]?.name} pazarın %{brandAnalytics.kpis.leaderShare}'ini kontrol ediyor
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Segment Analizi</p>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Segment Analizi</p>
|
||||
<p className="text-lg font-bold text-purple-600 mt-1">
|
||||
{Object.entries(brandAnalytics.priceSegments)
|
||||
.sort((a, b) => b[1].length - a[1].length)[0][0]}
|
||||
@@ -521,14 +484,14 @@ export default function BrandTab({ brandAnalytics, sortedBrands, handleBrandSort
|
||||
.sort((a, b) => b[1].length - a[1].length)[0][1].length} marka)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Pazar Yapısı</p>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Pazar Yapısı</p>
|
||||
<p className="text-lg font-bold text-green-600 mt-1">
|
||||
{brandAnalytics.kpis.marketConcentration} - HHI: {brandAnalytics.kpis.hhi}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Rekabet Yoğunluğu</p>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Rekabet Yoğunluğu</p>
|
||||
<p className="text-lg font-bold text-orange-600 mt-1">
|
||||
Top 3 marka toplam satışın %{brandAnalytics.kpis.top3Share}'i
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, PieChart, Pie, Cell, Legend, ScatterChart, Scatter, ZAxis } from 'recharts'
|
||||
import KpiCard from '../ui/KpiCard'
|
||||
import { Grid3X3, Trophy, BarChart3, PieChart as PieChartIcon } from 'lucide-react'
|
||||
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
|
||||
|
||||
export default function CategoryTab({ categoryAnalytics, sortedCategories, handleCategorySort, categorySortConfig }) {
|
||||
if (!categoryAnalytics) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Category analizi yükleniyor...</p>
|
||||
<p className="text-slate-400">Category analizi yükleniyor...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,95 +16,66 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
<div className="space-y-6">
|
||||
{/* Row 1: KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm font-medium">Toplam Kategori</p>
|
||||
<p className="text-3xl font-bold mt-2">{categoryAnalytics.kpis.totalCategories}</p>
|
||||
</div>
|
||||
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm font-medium">Lider Kategori Payı</p>
|
||||
<p className="text-3xl font-bold mt-2">%{categoryAnalytics.kpis.leaderShare}</p>
|
||||
</div>
|
||||
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100 text-sm font-medium">Ort. Ürün/Kategori</p>
|
||||
<p className="text-3xl font-bold mt-2">{categoryAnalytics.kpis.avgProductsPerCategory}</p>
|
||||
</div>
|
||||
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100 text-sm font-medium">Pazar Yoğunlaşması</p>
|
||||
<p className="text-2xl font-bold mt-2">{categoryAnalytics.kpis.marketConcentration}</p>
|
||||
<p className="text-orange-100 text-xs mt-1">HHI: {categoryAnalytics.kpis.hhi}</p>
|
||||
</div>
|
||||
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Toplam Kategori"
|
||||
value={categoryAnalytics.kpis.totalCategories}
|
||||
icon={Grid3X3}
|
||||
color="blue"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Lider Kategori Payı"
|
||||
value={`%${categoryAnalytics.kpis.leaderShare}`}
|
||||
icon={Trophy}
|
||||
color="violet"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Ort. Ürün/Kategori"
|
||||
value={categoryAnalytics.kpis.avgProductsPerCategory}
|
||||
icon={BarChart3}
|
||||
color="emerald"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Pazar Yoğunlaşması"
|
||||
value={categoryAnalytics.kpis.marketConcentration}
|
||||
subtitle={`HHI: ${categoryAnalytics.kpis.hhi}`}
|
||||
icon={PieChartIcon}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Top Categories Table + Price Positioning */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* En Çok Satan Kategoriler */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satan Kategoriler (Top 20)</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satan Kategoriler (Top 20)</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="border-b border-gray-200">
|
||||
<thead className="border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">#</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">Kategori</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Satış</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Ciro</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-500">Pay %</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-slate-400">#</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-slate-400">Kategori</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-slate-400">Satış</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-slate-400">Ciro</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-slate-400">Pay %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{categoryAnalytics.topByOrders.slice(0, 20).map((category, index) => (
|
||||
<tr key={category.name} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm text-gray-900 font-medium">{index + 1}</td>
|
||||
<tr key={category.name} className="hover:bg-orange-50/30">
|
||||
<td className="px-4 py-3 text-sm text-slate-900 font-medium">{index + 1}</td>
|
||||
<td className="px-4 py-3">
|
||||
<a
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-700 font-medium"
|
||||
className="text-orange-500 hover:text-orange-600 font-medium"
|
||||
>
|
||||
{category.name}
|
||||
</a>
|
||||
<div className="text-xs text-gray-600 mt-1">{category.productCount} ürün</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{category.productCount} ürün</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">{category.totalOrders.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">₺{category.totalRevenue.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">{category.totalOrders.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">₺{category.totalRevenue.toLocaleString('tr-TR')}</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600">{category.marketShare.toFixed(1)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -111,11 +85,11 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
</div>
|
||||
|
||||
{/* Fiyat Pozisyonlaması */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat Pozisyonlaması</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat Pozisyonlaması</h3>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(categoryAnalytics.priceSegments).map(([segment, categories]) => (
|
||||
<div key={segment} className="bg-gray-50 rounded-lg p-4">
|
||||
<div key={segment} className="bg-slate-50 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-3 py-1 text-xs font-medium rounded-full ${
|
||||
@@ -127,15 +101,15 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
}`}>
|
||||
{segment}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-700">
|
||||
<span className="text-sm font-semibold text-slate-700">
|
||||
{categories.length} kategori
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600">
|
||||
<span className="text-sm text-slate-500">
|
||||
%{Math.round((categories.length / categoryAnalytics.categories.length) * 100)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="w-full bg-slate-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
segment === 'Premium'
|
||||
@@ -147,7 +121,7 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
style={{ width: `${(categories.length / categoryAnalytics.categories.length) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
<p className="text-xs text-slate-400 mt-2">
|
||||
Toplam satış: {categories.reduce((sum, c) => sum + c.totalOrders, 0).toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -159,8 +133,8 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
{/* Row 3: Market Share Chart + Price/Performance Matrix */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Pazar Payı Dağılımı */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Kategori Pazar Payı Dağılımı</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Kategori Pazar Payı Dağılımı</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
@@ -185,21 +159,21 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
dataKey="value"
|
||||
>
|
||||
{[0, 1, 2, 3, 4, 5].map((index) => (
|
||||
<Cell key={`cell-${index}`} fill={['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#6b7280'][index]} />
|
||||
<Cell key={`cell-${index}`} fill={CHART_COLORS[index]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(value) => value.toLocaleString('tr-TR')} />
|
||||
<Tooltip {...CHART_TOOLTIP_STYLE} formatter={(value) => value.toLocaleString('tr-TR')} />
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="text-xs text-gray-500 mt-2 text-center">
|
||||
<p className="text-xs text-slate-400 mt-2 text-center">
|
||||
Pazar payı satış adedine göre hesaplanmıştır
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fiyat/Performans Matrisi */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fiyat/Performans Matrisi</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Fiyat/Performans Matrisi</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<CartesianGrid />
|
||||
@@ -212,11 +186,11 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white p-3 border border-gray-300 rounded shadow-lg">
|
||||
<p className="font-semibold text-gray-900">{data.name}</p>
|
||||
<p className="text-sm text-gray-600">Ort. Fiyat: ₺{data.avgPrice.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-sm text-gray-600">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-sm text-gray-600">Sosyal Skor: {data.socialScore}</p>
|
||||
<div className="bg-slate-800 p-3 border-none rounded-xl shadow-lg">
|
||||
<p className="font-semibold text-slate-50">{data.name}</p>
|
||||
<p className="text-sm text-slate-200">Ort. Fiyat: ₺{data.avgPrice.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-sm text-slate-200">Satış: {data.totalOrders.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-sm text-slate-200">Sosyal Skor: {data.socialScore}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -227,27 +201,27 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
<Scatter name="Kategoriler" data={categoryAnalytics.topByOrders.slice(0, 15)} fill="#8b5cf6" />
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="text-xs text-gray-500 mt-2 text-center">
|
||||
<p className="text-xs text-slate-400 mt-2 text-center">
|
||||
Kabarcık boyutu: Sosyal kanıt skoru (görüntülenme + satış + ürün çeşitliliği)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Detailed Category Comparison Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Kategori Karşılaştırması</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Kategori Karşılaştırması</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th
|
||||
onClick={() => handleCategorySort('name')}
|
||||
className="px-4 py-3 text-left text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-left text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Kategori
|
||||
{categorySortConfig.key === 'name' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -255,12 +229,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleCategorySort('totalOrders')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Satış
|
||||
{categorySortConfig.key === 'totalOrders' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -268,12 +242,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleCategorySort('totalRevenue')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Ciro
|
||||
{categorySortConfig.key === 'totalRevenue' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -281,12 +255,12 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleCategorySort('brandCount')}
|
||||
className="px-4 py-3 text-center text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-center text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
Marka Çeşitliliği
|
||||
{categorySortConfig.key === 'brandCount' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
@@ -294,35 +268,35 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleCategorySort('avgPrice')}
|
||||
className="px-4 py-3 text-right text-xs font-medium text-gray-500 cursor-pointer hover:bg-gray-100 select-none"
|
||||
className="px-4 py-3 text-right text-xs font-medium text-slate-400 cursor-pointer hover:bg-orange-50/30 select-none"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
Ort. Fiyat
|
||||
{categorySortConfig.key === 'avgPrice' && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-slate-400">
|
||||
{categorySortConfig.direction === 'desc' ? '↓' : '↑'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500">Fiyat Aralığı</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500">Segment</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">Fiyat Aralığı</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-400">Segment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{sortedCategories.map(category => (
|
||||
<tr key={category.name} className="hover:bg-gray-50">
|
||||
<tr key={category.name} className="hover:bg-orange-50/30">
|
||||
<td className="px-4 py-3">
|
||||
<a
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
|
||||
className="text-sm font-medium text-orange-500 hover:text-orange-600 hover:underline"
|
||||
>
|
||||
{category.name}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-slate-900">
|
||||
{category.totalOrders.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-green-600">
|
||||
@@ -334,10 +308,10 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
<span className="text-xs text-indigo-600">marka</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">
|
||||
₺{category.avgPrice.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-xs text-gray-600">
|
||||
<td className="px-4 py-3 text-right text-xs text-slate-500">
|
||||
{category.minPrice !== category.maxPrice
|
||||
? `₺${category.minPrice.toLocaleString('tr-TR')} - ₺${category.maxPrice.toLocaleString('tr-TR')}`
|
||||
: `₺${category.minPrice.toLocaleString('tr-TR')}`
|
||||
@@ -362,19 +336,19 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
</div>
|
||||
|
||||
{/* Row 5: Category Insights */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">
|
||||
Kategori İçgörüleri
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Lider Kategori Dominansı</p>
|
||||
<p className="text-lg font-bold text-blue-600 mt-1">
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Lider Kategori Dominansı</p>
|
||||
<p className="text-lg font-bold text-orange-500 mt-1">
|
||||
{categoryAnalytics.topByOrders[0]?.name} pazarın %{categoryAnalytics.kpis.leaderShare}'ini kontrol ediyor
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Segment Analizi</p>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Segment Analizi</p>
|
||||
<p className="text-lg font-bold text-purple-600 mt-1">
|
||||
{Object.entries(categoryAnalytics.priceSegments)
|
||||
.sort((a, b) => b[1].length - a[1].length)[0][0]}
|
||||
@@ -382,14 +356,14 @@ export default function CategoryTab({ categoryAnalytics, sortedCategories, handl
|
||||
.sort((a, b) => b[1].length - a[1].length)[0][1].length} kategori)
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Pazar Yapısı</p>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Pazar Yapısı</p>
|
||||
<p className="text-lg font-bold text-green-600 mt-1">
|
||||
{categoryAnalytics.kpis.marketConcentration} - HHI: {categoryAnalytics.kpis.hhi}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-gray-700">Rekabet Yoğunluğu</p>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-700">Rekabet Yoğunluğu</p>
|
||||
<p className="text-lg font-bold text-orange-600 mt-1">
|
||||
Top 3 kategori toplam satışın %{categoryAnalytics.kpis.top3Share}'i
|
||||
</p>
|
||||
|
||||
316
admin-panel/src/components/dashboard-tabs/HiddenChampionsTab.jsx
Normal file
316
admin-panel/src/components/dashboard-tabs/HiddenChampionsTab.jsx
Normal 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
245
admin-panel/src/components/dashboard-tabs/OpportunityTab.jsx
Normal file
245
admin-panel/src/components/dashboard-tabs/OpportunityTab.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ScatterChart, Scatter } from 'recharts'
|
||||
import KpiCard from '../ui/KpiCard'
|
||||
import { Globe, Flag, Ship, ShoppingBag } from 'lucide-react'
|
||||
import { CHART_COLORS, CHART_TOOLTIP_STYLE } from '../../constants/chartColors'
|
||||
|
||||
export default function OriginTab({ originAnalytics }) {
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null })
|
||||
@@ -65,7 +68,7 @@ export default function OriginTab({ originAnalytics }) {
|
||||
console.warn('⚠️ [ORIGINTAB] No originAnalytics data - showing loading state')
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Menşei analizi yükleniyor...</p>
|
||||
<p className="text-slate-400">Menşei analizi yükleniyor...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -116,82 +119,47 @@ export default function OriginTab({ originAnalytics }) {
|
||||
<div className="space-y-6">
|
||||
{/* Row 1: KPI Cards - 4 cards in a grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Card 1: Toplam Ülke Sayısı */}
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm font-medium">Toplam Ülke Sayısı</p>
|
||||
<p className="text-3xl font-bold mt-2">{countries?.length || 0}</p>
|
||||
</div>
|
||||
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Yerli Ürün Payı */}
|
||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100 text-sm font-medium">Yerli Ürün Payı</p>
|
||||
<p className="text-3xl font-bold mt-2">{kpis?.domesticPercentage || 0}%</p>
|
||||
<p className="text-green-100 text-xs mt-1">{(domesticData?.count || 0).toLocaleString('tr-TR')} ürün</p>
|
||||
</div>
|
||||
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3: İthal Ürün Payı */}
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm font-medium">İthal Ürün Payı</p>
|
||||
<p className="text-3xl font-bold mt-2">{kpis?.importPercentage || 0}%</p>
|
||||
<p className="text-purple-100 text-xs mt-1">{(importData?.count || 0).toLocaleString('tr-TR')} ürün</p>
|
||||
</div>
|
||||
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 4: Toplam Satış */}
|
||||
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100 text-sm font-medium">Toplam Satış</p>
|
||||
<p className="text-3xl font-bold mt-2">{(totalOrders || 0).toLocaleString('tr-TR')}</p>
|
||||
<p className="text-orange-100 text-xs mt-1">Tüm ülkeler</p>
|
||||
</div>
|
||||
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Toplam Ülke Sayısı"
|
||||
value={countries?.length || 0}
|
||||
icon={Globe}
|
||||
color="blue"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Yerli Ürün Payı"
|
||||
value={`${kpis?.domesticPercentage || 0}%`}
|
||||
subtitle={`${(domesticData?.count || 0).toLocaleString('tr-TR')} ürün`}
|
||||
icon={Flag}
|
||||
color="emerald"
|
||||
/>
|
||||
<KpiCard
|
||||
title="İthal Ürün Payı"
|
||||
value={`${kpis?.importPercentage || 0}%`}
|
||||
subtitle={`${(importData?.count || 0).toLocaleString('tr-TR')} ürün`}
|
||||
icon={Ship}
|
||||
color="violet"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Toplam Satış"
|
||||
value={(totalOrders || 0).toLocaleString('tr-TR')}
|
||||
subtitle="Tüm ülkeler"
|
||||
icon={ShoppingBag}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Kategori-Ülke Isı Haritası (Top 10x10) - Full Width */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4">
|
||||
Kategori-Ülke Isı Haritası (Top 10x10)
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-200 bg-gray-50 p-2 text-left font-semibold text-gray-700 sticky left-0 z-10">Kategori</th>
|
||||
<th className="border border-slate-200 bg-slate-50 p-2 text-left font-semibold text-slate-700 sticky left-0 z-10">Kategori</th>
|
||||
{topCountriesForHeatmap.map(country => (
|
||||
<th key={country} className="border border-gray-200 bg-gray-50 p-2 text-center font-semibold text-gray-700 min-w-[100px]">
|
||||
<th key={country} className="border border-slate-200 bg-slate-50 p-2 text-center font-semibold text-slate-700 min-w-[100px]">
|
||||
{country}
|
||||
</th>
|
||||
))}
|
||||
@@ -200,7 +168,7 @@ export default function OriginTab({ originAnalytics }) {
|
||||
<tbody>
|
||||
{topCategories.map(category => (
|
||||
<tr key={category}>
|
||||
<td className="border border-gray-200 p-2 font-medium text-gray-700 bg-gray-50 sticky left-0 z-10">{category}</td>
|
||||
<td className="border border-slate-200 p-2 font-medium text-slate-700 bg-slate-50 sticky left-0 z-10">{category}</td>
|
||||
{topCountriesForHeatmap.map(country => {
|
||||
const cellData = categoryCountryMatrix[category]?.[country]
|
||||
const count = cellData?.count || 0
|
||||
@@ -217,7 +185,7 @@ export default function OriginTab({ originAnalytics }) {
|
||||
else if (count > 0) bgColor = 'bg-blue-50'
|
||||
|
||||
return (
|
||||
<td key={country} className={`border border-gray-200 p-2 text-center ${bgColor}`}>
|
||||
<td key={country} className={`border border-slate-200 p-2 text-center ${bgColor}`}>
|
||||
{count > 0 ? (
|
||||
<div>
|
||||
<div className="text-xs font-semibold">Satış: {orders.toLocaleString('tr-TR')}</div>
|
||||
@@ -238,26 +206,26 @@ export default function OriginTab({ originAnalytics }) {
|
||||
</div>
|
||||
|
||||
{/* Row 3: En Çok Satan Ülkeler (Top 20) - Full Width, 10x10 Grid */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4">
|
||||
En Çok Satan Ülkeler (Top 20)
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Left Column: Countries 1-10 */}
|
||||
<div className="space-y-2">
|
||||
{(topByOrders || []).slice(0, 10).map((item, index) => (
|
||||
<div key={item.country} className="flex items-center justify-between p-3 bg-gradient-to-r from-blue-50 to-transparent rounded-lg hover:from-blue-100 transition-colors">
|
||||
<div key={item.country} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-orange-50/30 transition-colors">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-blue-500 text-white rounded-full font-bold text-sm">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-orange-500 text-white rounded-full font-bold text-sm">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-800">{item.country}</span>
|
||||
<span className="font-semibold text-slate-800">{item.country}</span>
|
||||
</div>
|
||||
<div className="text-right space-y-1">
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Satış:</span> {item.totalOrders.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Ciro:</span> ₺{item.totalRevenue.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -268,18 +236,18 @@ export default function OriginTab({ originAnalytics }) {
|
||||
{/* Right Column: Countries 11-20 */}
|
||||
<div className="space-y-2">
|
||||
{(topByOrders || []).slice(10, 20).map((item, index) => (
|
||||
<div key={item.country} className="flex items-center justify-between p-3 bg-gradient-to-r from-purple-50 to-transparent rounded-lg hover:from-purple-100 transition-colors">
|
||||
<div key={item.country} className="flex items-center justify-between p-3 bg-gradient-to-r from-purple-50 to-transparent rounded-xl hover:from-purple-100 transition-colors">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-purple-500 text-white rounded-full font-bold text-sm">
|
||||
{index + 11}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-800">{item.country}</span>
|
||||
<span className="font-semibold text-slate-800">{item.country}</span>
|
||||
</div>
|
||||
<div className="text-right space-y-1">
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Satış:</span> {item.totalOrders.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-slate-900">
|
||||
<span className="font-semibold">Ciro:</span> ₺{item.totalRevenue.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -292,8 +260,8 @@ export default function OriginTab({ originAnalytics }) {
|
||||
{/* Row 4: Two Charts Side by Side (50%-50%) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Chart 1: Ülke Bazlı Satış (Bar Chart) */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4">
|
||||
Ülke Bazlı Satış (Top 15)
|
||||
</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@@ -308,22 +276,17 @@ export default function OriginTab({ originAnalytics }) {
|
||||
/>
|
||||
<YAxis tick={{ fontSize: 11 }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
{...CHART_TOOLTIP_STYLE}
|
||||
formatter={(value) => value.toLocaleString('tr-TR')}
|
||||
/>
|
||||
<Bar dataKey="totalOrders" fill="#3b82f6" radius={[8, 8, 0, 0]} />
|
||||
<Bar dataKey="totalOrders" fill={CHART_COLORS[0]} radius={[8, 8, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Chart 2: Ortalama Fiyat / Ciro İlişkisi (Scatter Chart) */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4">
|
||||
Ciro / Satış İlişkisi (Top 15)
|
||||
</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
@@ -349,12 +312,12 @@ export default function OriginTab({ originAnalytics }) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3">
|
||||
<p className="font-semibold text-gray-900 mb-2 text-sm">{data.name}</p>
|
||||
<p className="font-semibold text-gray-900 text-sm">
|
||||
<div className="bg-slate-800 border-none rounded-xl shadow-lg p-3">
|
||||
<p className="font-semibold text-slate-50 mb-2 text-sm">{data.name}</p>
|
||||
<p className="font-semibold text-slate-200 text-sm">
|
||||
Ciro: ₺{data.totalRevenue.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
<p className="font-semibold text-gray-900 text-sm">
|
||||
<p className="font-semibold text-slate-200 text-sm">
|
||||
Satış: {data.totalOrders.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -377,70 +340,70 @@ export default function OriginTab({ originAnalytics }) {
|
||||
</div>
|
||||
|
||||
{/* Row 5: Detaylı Ülke Karşılaştırma Tablosu */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Detaylı Ülke Karşılaştırma</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Detaylı Ülke Karşılaştırma</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="border-b-2 border-gray-200 bg-gray-50">
|
||||
<thead className="border-b-2 border-slate-200 bg-slate-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-4 py-3 text-left text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-left text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('country')}
|
||||
>
|
||||
Ülke{renderSortIndicator('country')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('count')}
|
||||
>
|
||||
Ürün{renderSortIndicator('count')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('totalOrders')}
|
||||
>
|
||||
Satış{renderSortIndicator('totalOrders')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('totalRevenue')}
|
||||
>
|
||||
Ciro{renderSortIndicator('totalRevenue')}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">Pay %</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">Pay %</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-right text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('avgPrice')}
|
||||
>
|
||||
Ort. Fiyat{renderSortIndicator('avgPrice')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('categoryCount')}
|
||||
>
|
||||
Kategori{renderSortIndicator('categoryCount')}
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
className="px-4 py-3 text-center text-sm font-semibold text-slate-700 cursor-pointer hover:bg-orange-50/30 transition-colors"
|
||||
onClick={() => handleSort('brandCount')}
|
||||
>
|
||||
Marka{renderSortIndicator('brandCount')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{getSortedData().map((item) => (
|
||||
<tr key={item.country} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm font-semibold text-gray-900">
|
||||
<tr key={item.country} className="hover:bg-orange-50/30">
|
||||
<td className="px-4 py-3 text-sm font-semibold text-slate-900">
|
||||
{item.country}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-gray-900">
|
||||
<td className="px-4 py-3 text-sm text-right text-slate-900">
|
||||
{item.count.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right font-semibold text-gray-900">
|
||||
<td className="px-4 py-3 text-sm text-right font-semibold text-slate-900">
|
||||
{item.totalOrders.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right font-semibold text-gray-900">
|
||||
<td className="px-4 py-3 text-sm text-right font-semibold text-slate-900">
|
||||
₺{item.totalRevenue.toLocaleString('tr-TR')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right">
|
||||
@@ -448,11 +411,11 @@ export default function OriginTab({ originAnalytics }) {
|
||||
{((item.totalOrders / totalOrders) * 100).toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm text-slate-900">
|
||||
₺{item.avgPrice.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-gray-900">{item.categoryCount || 0}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-gray-900">{item.brandCount || 0}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-slate-900">{item.categoryCount || 0}</td>
|
||||
<td className="px-4 py-3 text-center text-sm text-slate-900">{item.brandCount || 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,109 +1,381 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Package, ShoppingCart, Eye, DollarSign, Tag, TrendingUp, Swords } from 'lucide-react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell } from 'recharts'
|
||||
import KpiCard from '../ui/KpiCard'
|
||||
import { API_URL, fetchWithTimeout, TIMEOUT_CONFIG } from '../../config/api'
|
||||
|
||||
// Competition Score gauge component
|
||||
function CompetitionGauge({ score }) {
|
||||
const radius = 60
|
||||
const circumference = Math.PI * radius
|
||||
const offset = circumference - (score / 100) * circumference
|
||||
const color = score >= 67 ? '#ef4444' : score >= 34 ? '#f59e0b' : '#22c55e'
|
||||
const label = score >= 67 ? 'Yoğun Rekabet' : score >= 34 ? 'Orta Rekabet' : 'Düşük Rekabet (Fırsat)'
|
||||
const bgColor = score >= 67 ? 'bg-red-50 text-red-700' : score >= 34 ? 'bg-amber-50 text-amber-700' : 'bg-emerald-50 text-emerald-700'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<svg width="140" height="80" viewBox="0 0 140 80">
|
||||
<path
|
||||
d="M 10 75 A 60 60 0 0 1 130 75"
|
||||
fill="none"
|
||||
stroke="#e2e8f0"
|
||||
strokeWidth="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M 10 75 A 60 60 0 0 1 130 75"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="10"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
style={{ transition: 'stroke-dashoffset 1s ease' }}
|
||||
/>
|
||||
<text x="70" y="65" textAnchor="middle" className="text-2xl font-bold" fill="#1e293b">
|
||||
{Math.round(score)}
|
||||
</text>
|
||||
</svg>
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full mt-1 ${bgColor}`}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function OverviewTab({
|
||||
overviewKPIs,
|
||||
topSellingProducts,
|
||||
topSellingBrands,
|
||||
topSellingCategories,
|
||||
mostViewedCategories
|
||||
mostViewedCategories,
|
||||
reportId,
|
||||
allProducts
|
||||
}) {
|
||||
// Sales Funnel state
|
||||
const [salesData, setSalesData] = useState(null)
|
||||
const [salesLoading, setSalesLoading] = useState(false)
|
||||
|
||||
// Fetch sales analytics
|
||||
useEffect(() => {
|
||||
if (!reportId) return
|
||||
setSalesLoading(true)
|
||||
fetchWithTimeout(
|
||||
`${API_URL}/api/reports/${reportId}/sales-analytics`,
|
||||
{},
|
||||
TIMEOUT_CONFIG.DASHBOARD
|
||||
)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Sales data failed')
|
||||
return res.json()
|
||||
})
|
||||
.then(data => setSalesData(data))
|
||||
.catch(() => {}) // silently fail - optional feature
|
||||
.finally(() => setSalesLoading(false))
|
||||
}, [reportId])
|
||||
|
||||
// Price Distribution histogram
|
||||
const priceDistribution = useMemo(() => {
|
||||
if (!allProducts?.length) return null
|
||||
|
||||
const prices = allProducts.map(p => p.price || 0).filter(p => p > 0)
|
||||
if (prices.length === 0) return null
|
||||
|
||||
const min = Math.min(...prices)
|
||||
const max = Math.max(...prices)
|
||||
const mean = prices.reduce((s, p) => s + p, 0) / prices.length
|
||||
const sortedPrices = [...prices].sort((a, b) => a - b)
|
||||
const median = sortedPrices.length % 2 === 0
|
||||
? (sortedPrices[sortedPrices.length / 2 - 1] + sortedPrices[sortedPrices.length / 2]) / 2
|
||||
: sortedPrices[Math.floor(sortedPrices.length / 2)]
|
||||
|
||||
const bucketCount = 10
|
||||
const range = max - min || 1
|
||||
const bucketSize = range / bucketCount
|
||||
|
||||
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
|
||||
range: `₺${Math.round(min + i * bucketSize)}-${Math.round(min + (i + 1) * bucketSize)}`,
|
||||
min: min + i * bucketSize,
|
||||
max: min + (i + 1) * bucketSize,
|
||||
count: 0
|
||||
}))
|
||||
|
||||
prices.forEach(price => {
|
||||
const idx = Math.min(Math.floor((price - min) / bucketSize), bucketCount - 1)
|
||||
buckets[idx].count++
|
||||
})
|
||||
|
||||
return { buckets, mean: Math.round(mean), median: Math.round(median) }
|
||||
}, [allProducts])
|
||||
|
||||
// Competition Score
|
||||
const competitionScore = useMemo(() => {
|
||||
if (!allProducts?.length) return null
|
||||
|
||||
const products = allProducts
|
||||
const totalProducts = products.length
|
||||
const uniqueBrands = new Set(products.map(p => p.brand).filter(Boolean)).size
|
||||
|
||||
// Brand diversity: unique_brands / total_products * 30
|
||||
const brandDiversity = Math.min(30, (uniqueBrands / totalProducts) * 30)
|
||||
|
||||
// Price variance: std_dev / mean * 20
|
||||
const prices = products.map(p => p.price || 0).filter(p => p > 0)
|
||||
const meanPrice = prices.length > 0 ? prices.reduce((s, p) => s + p, 0) / prices.length : 0
|
||||
const variance = prices.length > 0
|
||||
? prices.reduce((s, p) => s + Math.pow(p - meanPrice, 2), 0) / prices.length
|
||||
: 0
|
||||
const stdDev = Math.sqrt(variance)
|
||||
const priceVariance = meanPrice > 0 ? Math.min(20, (stdDev / meanPrice) * 20) : 0
|
||||
|
||||
// Product density: log(total_products) * 10
|
||||
const productDensity = Math.min(10, Math.log10(Math.max(1, totalProducts)) * 10)
|
||||
|
||||
// HHI inverse: (1 - HHI/10000) * 40
|
||||
const brandOrders = {}
|
||||
const totalOrders = products.reduce((s, p) => {
|
||||
const b = p.brand || 'Unknown'
|
||||
brandOrders[b] = (brandOrders[b] || 0) + (p.orders || 0)
|
||||
return s + (p.orders || 0)
|
||||
}, 0)
|
||||
|
||||
let hhi = 0
|
||||
if (totalOrders > 0) {
|
||||
Object.values(brandOrders).forEach(orders => {
|
||||
const share = (orders / totalOrders) * 100
|
||||
hhi += share * share
|
||||
})
|
||||
}
|
||||
const hhiInverse = Math.max(0, (1 - hhi / 10000) * 40)
|
||||
|
||||
const score = Math.min(100, Math.max(0, brandDiversity + priceVariance + productDensity + hhiInverse))
|
||||
|
||||
return { score, brandDiversity, priceVariance, productDensity, hhiInverse, hhi: Math.round(hhi) }
|
||||
}, [allProducts])
|
||||
|
||||
if (!overviewKPIs) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-gray-500">Genel bakış verileri yükleniyor...</p>
|
||||
<p className="text-slate-400">Genel bakış verileri yükleniyor...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Sales funnel data
|
||||
const funnelData = salesData ? [
|
||||
{ name: 'Görüntüleme', value: salesData.total_views || salesData.totalViews || 0, color: '#6366f1' },
|
||||
{ name: 'Sepet', value: salesData.total_baskets || salesData.totalBaskets || 0, color: '#f59e0b' },
|
||||
{ name: 'Sipariş', value: salesData.total_orders || salesData.totalOrders || 0, color: '#22c55e' },
|
||||
] : null
|
||||
|
||||
const conversionRates = salesData ? {
|
||||
viewToBasket: salesData.view_to_basket_rate || salesData.viewToBasketRate || 0,
|
||||
basketToOrder: salesData.basket_to_order_rate || salesData.basketToOrderRate || 0,
|
||||
viewToOrder: salesData.view_to_order_rate || salesData.viewToOrderRate || 0,
|
||||
} : null
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm font-medium">Toplam Ürün</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{overviewKPIs.totalProducts.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-blue-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<KpiCard
|
||||
title="Toplam Ürün"
|
||||
value={overviewKPIs.totalProducts.toLocaleString('tr-TR')}
|
||||
icon={Package}
|
||||
color="blue"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Toplam Satın Alma"
|
||||
value={overviewKPIs.totalOrders.toLocaleString('tr-TR')}
|
||||
icon={ShoppingCart}
|
||||
color="emerald"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Toplam Görüntülenme"
|
||||
value={overviewKPIs.totalViews.toLocaleString('tr-TR')}
|
||||
icon={Eye}
|
||||
color="violet"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Toplam Ciro"
|
||||
value={`₺${(overviewKPIs.totalRevenue || 0).toLocaleString('tr-TR')}`}
|
||||
icon={DollarSign}
|
||||
color="orange"
|
||||
/>
|
||||
<KpiCard
|
||||
title="Ortalama Fiyat"
|
||||
value={`₺${overviewKPIs.avgPrice.toLocaleString('tr-TR')}`}
|
||||
icon={Tag}
|
||||
color="rose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100 text-sm font-medium">Toplam Satın Alma</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{overviewKPIs.totalOrders.toLocaleString('tr-TR')}
|
||||
{/* Competition Score */}
|
||||
{competitionScore && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col items-center justify-center">
|
||||
<h3 className="text-sm font-medium text-slate-500 mb-3">Rekabet Skoru</h3>
|
||||
<CompetitionGauge score={competitionScore.score} />
|
||||
<p className="text-xs text-slate-400 mt-3 text-center">
|
||||
HHI: {competitionScore.hhi.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-green-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-sm font-medium text-slate-500 mb-4">Rekabet Bileşenleri</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: 'Marka Çeşitliliği', value: competitionScore.brandDiversity, max: 30, color: 'bg-blue-500' },
|
||||
{ label: 'Fiyat Varyansı', value: competitionScore.priceVariance, max: 20, color: 'bg-amber-500' },
|
||||
{ label: 'Ürün Yoğunluğu', value: competitionScore.productDensity, max: 10, color: 'bg-violet-500' },
|
||||
{ label: 'HHI Ters (Dağılım)', value: competitionScore.hhiInverse, max: 40, color: 'bg-emerald-500' },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-slate-600">{item.label}</span>
|
||||
<span className="text-xs font-medium text-slate-900">{item.value.toFixed(1)} / {item.max}</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded-full h-2">
|
||||
<div
|
||||
className={`${item.color} h-2 rounded-full transition-all`}
|
||||
style={{ width: `${(item.value / item.max) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sales Funnel */}
|
||||
{funnelData && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Satış Hunisi</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Funnel Bar Chart */}
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={funnelData} layout="vertical" margin={{ left: 20, right: 30 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" horizontal={false} />
|
||||
<XAxis type="number" tick={{ fill: '#64748b', fontSize: 12 }} />
|
||||
<YAxis dataKey="name" type="category" tick={{ fill: '#64748b', fontSize: 12 }} width={90} />
|
||||
<Tooltip
|
||||
formatter={(value) => [value.toLocaleString('tr-TR'), '']}
|
||||
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0' }}
|
||||
/>
|
||||
<Bar dataKey="value" radius={[0, 4, 4, 0]}>
|
||||
{funnelData.map((entry, index) => (
|
||||
<Cell key={index} fill={entry.color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm font-medium">Toplam Görüntülenme</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
{overviewKPIs.totalViews.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
{/* Conversion Rates */}
|
||||
{conversionRates && (
|
||||
<div className="flex flex-col justify-center space-y-4">
|
||||
<div className="flex items-center gap-3 p-3 bg-indigo-50/50 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-indigo-500" />
|
||||
<span className="text-sm text-slate-600 flex-1">Görüntüleme → Sepet</span>
|
||||
<span className="text-sm font-bold text-indigo-600">
|
||||
%{typeof conversionRates.viewToBasket === 'number' ? conversionRates.viewToBasket.toFixed(2) : conversionRates.viewToBasket}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-purple-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<div className="flex items-center gap-3 p-3 bg-amber-50/50 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-amber-500" />
|
||||
<span className="text-sm text-slate-600 flex-1">Sepet → Sipariş</span>
|
||||
<span className="text-sm font-bold text-amber-600">
|
||||
%{typeof conversionRates.basketToOrder === 'number' ? conversionRates.basketToOrder.toFixed(2) : conversionRates.basketToOrder}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-emerald-50/50 rounded-lg">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
<span className="text-sm text-slate-600 flex-1">Görüntüleme → Sipariş</span>
|
||||
<span className="text-sm font-bold text-emerald-600">
|
||||
%{typeof conversionRates.viewToOrder === 'number' ? conversionRates.viewToOrder.toFixed(2) : conversionRates.viewToOrder}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100 text-sm font-medium">Toplam Ciro</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
₺{(overviewKPIs.totalRevenue || 0).toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{/* Top Conversion Products */}
|
||||
{salesData?.top_conversion_products?.length > 0 && (
|
||||
<div className="mt-6 pt-4 border-t border-slate-100">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">En Yüksek Dönüşüm Oranı</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{salesData.top_conversion_products.slice(0, 10).map((p, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-2 bg-slate-50 rounded-lg text-sm">
|
||||
<span className="w-5 h-5 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold shrink-0">{i + 1}</span>
|
||||
<span className="truncate flex-1 text-slate-700">{p.name}</span>
|
||||
<span className="font-medium text-emerald-600 shrink-0">%{(p.conversion_rate || 0).toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gradient-to-br from-red-500 to-red-600 rounded-lg shadow-sm p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-red-100 text-sm font-medium">Ortalama Fiyat</p>
|
||||
<p className="text-3xl font-bold mt-2">
|
||||
₺{overviewKPIs.avgPrice.toLocaleString('tr-TR')}
|
||||
{/* Top Performance Products */}
|
||||
{salesData?.top_performance_products?.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-slate-100">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">En Yüksek Performans Skoru</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{salesData.top_performance_products.slice(0, 10).map((p, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-2 bg-slate-50 rounded-lg text-sm">
|
||||
<span className="w-5 h-5 bg-orange-500 text-white rounded-full flex items-center justify-center text-xs font-bold shrink-0">{i + 1}</span>
|
||||
<span className="truncate flex-1 text-slate-700">{p.name}</span>
|
||||
<span className="font-medium text-orange-600 shrink-0">{(p.performance_score || 0).toFixed(0)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Price Distribution */}
|
||||
{priceDistribution && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-1">Fiyat Dağılımı</h3>
|
||||
<p className="text-xs text-slate-400 mb-4">
|
||||
Ort: ₺{priceDistribution.mean.toLocaleString('tr-TR')} · Medyan: ₺{priceDistribution.median.toLocaleString('tr-TR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-red-400 bg-opacity-30 rounded-full p-3">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={priceDistribution.buckets} margin={{ top: 10, right: 30, left: 10, bottom: 20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="range"
|
||||
tick={{ fill: '#64748b', fontSize: 10 }}
|
||||
angle={-30}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
/>
|
||||
<YAxis tick={{ fill: '#64748b', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
formatter={(value) => [`${value} ürün`, 'Sayı']}
|
||||
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0' }}
|
||||
/>
|
||||
<ReferenceLine
|
||||
x={priceDistribution.buckets.findIndex(b => b.min <= priceDistribution.mean && b.max > priceDistribution.mean)}
|
||||
stroke="#f97316"
|
||||
strokeDasharray="5 5"
|
||||
label={{ value: `Ort: ₺${priceDistribution.mean}`, fill: '#f97316', fontSize: 11, position: 'top' }}
|
||||
/>
|
||||
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} label={{ position: 'top', fill: '#64748b', fontSize: 11 }} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Top Products & Top Brands */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* En Çok Satış Yapan Ürünler */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Ürünler</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Ürünler</h3>
|
||||
<div className="space-y-3">
|
||||
{topSellingProducts.map((product, index) => (
|
||||
<a
|
||||
@@ -111,9 +383,9 @@ export default function OverviewTab({
|
||||
href={product.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
<img
|
||||
@@ -122,12 +394,12 @@ export default function OverviewTab({
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{product.name}</p>
|
||||
<p className="text-xs text-gray-600">₺{product.price?.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-sm font-medium text-slate-900 truncate">{product.name}</p>
|
||||
<p className="text-xs text-slate-500">₺{product.price?.toLocaleString('tr-TR')}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-gray-900">{product.orders?.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-gray-600">satış</p>
|
||||
<p className="text-sm font-bold text-slate-900">{product.orders?.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-slate-500">satış</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
@@ -135,8 +407,8 @@ export default function OverviewTab({
|
||||
</div>
|
||||
|
||||
{/* En Çok Satış Yapan Marka */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Marka</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Marka</h3>
|
||||
<div className="space-y-3">
|
||||
{topSellingBrands.map((brand, index) => (
|
||||
<a
|
||||
@@ -144,17 +416,17 @@ export default function OverviewTab({
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(brand.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{brand.name}</p>
|
||||
<p className="text-sm font-medium text-slate-900">{brand.name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-gray-900">{brand.totalOrders.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-gray-600">toplam satış</p>
|
||||
<p className="text-sm font-bold text-slate-900">{brand.totalOrders.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-slate-500">toplam satış</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
@@ -165,8 +437,8 @@ export default function OverviewTab({
|
||||
{/* Row 3: Top Categories by Revenue & Most Viewed Categories */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* En Çok Satış Yapan Kategoriler */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Satış Yapan Kategoriler</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Satış Yapan Kategoriler</h3>
|
||||
<div className="space-y-3">
|
||||
{topSellingCategories.map((category, index) => (
|
||||
<a
|
||||
@@ -174,17 +446,17 @@ export default function OverviewTab({
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{category.name}</p>
|
||||
<p className="text-sm font-medium text-slate-900">{category.name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-gray-900">₺{category.revenue.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-gray-600">toplam ciro</p>
|
||||
<p className="text-sm font-bold text-slate-900">₺{category.revenue.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-slate-500">toplam ciro</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
@@ -192,8 +464,8 @@ export default function OverviewTab({
|
||||
</div>
|
||||
|
||||
{/* En Çok Görüntülenme Alan Kategoriler */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">En Çok Görüntülenme Alan Kategoriler</h3>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">En Çok Görüntülenme Alan Kategoriler</h3>
|
||||
<div className="space-y-3">
|
||||
{mostViewedCategories.map((category, index) => (
|
||||
<a
|
||||
@@ -201,17 +473,17 @@ export default function OverviewTab({
|
||||
href={`https://www.trendyol.com/sr?q=${encodeURIComponent(category.name)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg hover:bg-orange-50/30 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{category.name}</p>
|
||||
<p className="text-sm font-medium text-slate-900">{category.name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-gray-900">{category.views.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-gray-600">görüntülenme</p>
|
||||
<p className="text-sm font-bold text-slate-900">{category.views.toLocaleString('tr-TR')}</p>
|
||||
<p className="text-xs text-slate-500">görüntülenme</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||
import { Search, Star, X, Filter, SlidersHorizontal } from 'lucide-react'
|
||||
|
||||
export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
@@ -233,14 +234,14 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
if (!allProducts || allProducts.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-8 text-center">
|
||||
<div className="text-slate-400 mb-4">
|
||||
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Henüz Ürün Yok</h3>
|
||||
<p className="text-gray-500">Ürün verisi yüklendiğinde burada görüntülenecektir.</p>
|
||||
<h3 className="text-lg font-medium text-slate-900 mb-2">Henüz Ürün Yok</h3>
|
||||
<p className="text-slate-400">Ürün verisi yüklendiğinde burada görüntülenecektir.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -248,70 +249,68 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Quick Filters */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
|
||||
<h3 className="text-sm font-semibold text-slate-700 mb-3 flex items-center gap-2">
|
||||
<span>⚡</span>
|
||||
Hızlı Filtreler
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<button
|
||||
onClick={() => applyQuickFilter({ minOrders: '100', minPrice: '', maxPrice: '' })}
|
||||
className="p-3 rounded-lg border-2 border-green-500 bg-green-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
className="p-3 rounded-xl border-2 border-green-500 bg-green-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
>
|
||||
<div className="text-2xl mb-1">🔥</div>
|
||||
<div className="text-xs font-semibold text-gray-800">Çok Satanlar</div>
|
||||
<div className="text-xs text-gray-500 mt-1">(100+ satış)</div>
|
||||
<div className="text-xs font-semibold text-slate-800">Çok Satanlar</div>
|
||||
<div className="text-xs text-slate-400 mt-1">(100+ satış)</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyQuickFilter({ minViews: '5000', minPrice: '', maxPrice: '' })}
|
||||
className="p-3 rounded-lg border-2 border-purple-500 bg-purple-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
className="p-3 rounded-xl border-2 border-purple-500 bg-purple-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
>
|
||||
<div className="text-2xl mb-1">👁</div>
|
||||
<div className="text-xs font-semibold text-gray-800">Yüksek Görüntüleme</div>
|
||||
<div className="text-xs text-gray-500 mt-1">(5K+ görüntülenme)</div>
|
||||
<div className="text-xs font-semibold text-slate-800">Yüksek Görüntüleme</div>
|
||||
<div className="text-xs text-slate-400 mt-1">(5K+ görüntülenme)</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyQuickFilter({ minPrice: '1000', maxPrice: '', minOrders: '', minViews: '' })}
|
||||
className="p-3 rounded-lg border-2 border-orange-500 bg-orange-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
className="p-3 rounded-xl border-2 border-orange-500 bg-orange-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
>
|
||||
<div className="text-2xl mb-1">💎</div>
|
||||
<div className="text-xs font-semibold text-gray-800">Premium</div>
|
||||
<div className="text-xs text-gray-500 mt-1">(1000+ TL)</div>
|
||||
<div className="text-xs font-semibold text-slate-800">Premium</div>
|
||||
<div className="text-xs text-slate-400 mt-1">(1000+ TL)</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyQuickFilter({ maxPrice: '200', minPrice: '', minOrders: '', minViews: '' })}
|
||||
className="p-3 rounded-lg border-2 border-blue-500 bg-blue-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
className="p-3 rounded-xl border-2 border-blue-500 bg-blue-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
>
|
||||
<div className="text-2xl mb-1">💰</div>
|
||||
<div className="text-xs font-semibold text-gray-800">Ekonomik</div>
|
||||
<div className="text-xs text-gray-500 mt-1">(<200 TL)</div>
|
||||
<div className="text-xs font-semibold text-slate-800">Ekonomik</div>
|
||||
<div className="text-xs text-slate-400 mt-1">(<200 TL)</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyQuickFilter({ minReviews: '100', minPrice: '', maxPrice: '', minOrders: '', minViews: '' })}
|
||||
className="p-3 rounded-lg border-2 border-red-500 bg-red-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
className="p-3 rounded-xl border-2 border-red-500 bg-red-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
>
|
||||
<div className="text-2xl mb-1">💬</div>
|
||||
<div className="text-xs font-semibold text-gray-800">Çok Yorumlananlar</div>
|
||||
<div className="text-xs text-gray-500 mt-1">(100+ yorum)</div>
|
||||
<div className="text-xs font-semibold text-slate-800">Çok Yorumlananlar</div>
|
||||
<div className="text-xs text-slate-400 mt-1">(100+ yorum)</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyQuickFilter({ minRating: '4.5', minPrice: '', maxPrice: '', minOrders: '', minViews: '', minReviews: '' })}
|
||||
className="p-3 rounded-lg border-2 border-indigo-500 bg-indigo-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
className="p-3 rounded-xl border-2 border-indigo-500 bg-indigo-50 transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||
>
|
||||
<div className="text-2xl mb-1">⭐</div>
|
||||
<div className="text-xs font-semibold text-gray-800">Yüksek Puanlılar</div>
|
||||
<div className="text-xs text-gray-500 mt-1">(4.5+ puan)</div>
|
||||
<div className="text-xs font-semibold text-slate-800">Yüksek Puanlılar</div>
|
||||
<div className="text-xs text-slate-400 mt-1">(4.5+ puan)</div>
|
||||
</button>
|
||||
</div>
|
||||
{activeFilterCount > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<div className="mt-3 pt-3 border-t border-slate-200">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="w-full px-4 py-2 bg-red-50 text-red-700 rounded-xl hover:bg-red-100 transition-colors border border-red-200 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X size={16} />
|
||||
Filtreleri Temizle ({activeFilterCount})
|
||||
</button>
|
||||
</div>
|
||||
@@ -319,31 +318,31 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
</div>
|
||||
|
||||
{/* Filter Panel */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Search */}
|
||||
<div className="lg:col-span-2">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
🔎 Ürün, Marka veya Barkod Ara
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2 flex items-center gap-1">
|
||||
<Search size={12} /> Ürün, Marka veya Barkod Ara
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
placeholder="Örn: iPhone, Samsung, 8690000000000"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
📁 Kategori
|
||||
</label>
|
||||
<select
|
||||
value={filters.category}
|
||||
onChange={(e) => updateFilter('category', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
>
|
||||
<option value="">Tüm Kategoriler</option>
|
||||
{uniqueCategories.map(cat => (
|
||||
@@ -354,13 +353,13 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
🏷️ Marka
|
||||
</label>
|
||||
<select
|
||||
value={filters.brand}
|
||||
onChange={(e) => updateFilter('brand', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
>
|
||||
<option value="">Tüm Markalar</option>
|
||||
{uniqueBrands.map(brand => (
|
||||
@@ -371,7 +370,7 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Min Price */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
💵 Min Fiyat (TL)
|
||||
</label>
|
||||
<input
|
||||
@@ -380,13 +379,13 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onChange={(e) => updateFilter('minPrice', e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Price */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
💵 Max Fiyat (TL)
|
||||
</label>
|
||||
<input
|
||||
@@ -395,19 +394,19 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onChange={(e) => updateFilter('maxPrice', e.target.value)}
|
||||
placeholder="∞"
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Country */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
🌍 Ülke/Menşei
|
||||
</label>
|
||||
<select
|
||||
value={filters.country}
|
||||
onChange={(e) => updateFilter('country', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
>
|
||||
<option value="">Tüm Ülkeler</option>
|
||||
{uniqueCountries.map(country => (
|
||||
@@ -418,7 +417,7 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Min Orders */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
🛒 Min Satış (Son 3 Gün)
|
||||
</label>
|
||||
<input
|
||||
@@ -427,13 +426,13 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onChange={(e) => updateFilter('minOrders', e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Views */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
👁 Min Görüntülenme
|
||||
</label>
|
||||
<input
|
||||
@@ -442,13 +441,13 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onChange={(e) => updateFilter('minViews', e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Reviews */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
💬 Min Yorum Sayısı
|
||||
</label>
|
||||
<input
|
||||
@@ -457,13 +456,13 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onChange={(e) => updateFilter('minReviews', e.target.value)}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Reviews */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
💬 Max Yorum Sayısı
|
||||
</label>
|
||||
<input
|
||||
@@ -472,13 +471,13 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onChange={(e) => updateFilter('maxReviews', e.target.value)}
|
||||
placeholder="∞"
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Rating */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
||||
<label className="block text-xs font-medium text-slate-500 mb-2">
|
||||
⭐ Min Puan
|
||||
</label>
|
||||
<input
|
||||
@@ -489,21 +488,21 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
min="0"
|
||||
max="5"
|
||||
step="0.5"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort Controls */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<label className="text-xs font-medium text-gray-600">
|
||||
<label className="text-xs font-medium text-slate-500">
|
||||
Sıralama:
|
||||
</label>
|
||||
<select
|
||||
value={filters.sortBy}
|
||||
onChange={(e) => updateFilter('sortBy', e.target.value)}
|
||||
className="px-3 py-1.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="px-3 py-1.5 border border-slate-300 rounded-xl text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
>
|
||||
<option value="orders">Satış Sayısı</option>
|
||||
<option value="views">Görüntülenme</option>
|
||||
@@ -514,7 +513,7 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
<select
|
||||
value={filters.sortOrder}
|
||||
onChange={(e) => updateFilter('sortOrder', e.target.value)}
|
||||
className="px-3 py-1.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="px-3 py-1.5 border border-slate-300 rounded-xl text-sm focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
>
|
||||
<option value="desc">Azalan (Yüksek → Düşük)</option>
|
||||
<option value="asc">Artan (Düşük → Yüksek)</option>
|
||||
@@ -525,73 +524,73 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Active Filters */}
|
||||
{activeFilterCount > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium text-blue-900">Aktif Filtreler:</span>
|
||||
<span className="text-sm font-medium text-orange-900">Aktif Filtreler:</span>
|
||||
{filters.searchText && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Arama: "{filters.searchText}"
|
||||
<button onClick={() => setSearchText('')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => setSearchText('')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.category && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Kategori: {filters.category}
|
||||
<button onClick={() => updateFilter('category', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('category', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.brand && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Marka: {filters.brand}
|
||||
<button onClick={() => updateFilter('brand', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('brand', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.country && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Ülke: {filters.country}
|
||||
<button onClick={() => updateFilter('country', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('country', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.minPrice && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Min: ₺{parseFloat(filters.minPrice).toLocaleString('tr-TR')}
|
||||
<button onClick={() => updateFilter('minPrice', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('minPrice', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.maxPrice && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Max: ₺{parseFloat(filters.maxPrice).toLocaleString('tr-TR')}
|
||||
<button onClick={() => updateFilter('maxPrice', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('maxPrice', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.minOrders && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Min Satış: {parseInt(filters.minOrders).toLocaleString('tr-TR')}
|
||||
<button onClick={() => updateFilter('minOrders', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('minOrders', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.minViews && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Min Görüntülenme: {parseInt(filters.minViews).toLocaleString('tr-TR')}
|
||||
<button onClick={() => updateFilter('minViews', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('minViews', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.minReviews && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Min Yorum: {parseInt(filters.minReviews).toLocaleString('tr-TR')}
|
||||
<button onClick={() => updateFilter('minReviews', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('minReviews', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.maxReviews && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Max Yorum: {parseInt(filters.maxReviews).toLocaleString('tr-TR')}
|
||||
<button onClick={() => updateFilter('maxReviews', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('maxReviews', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
{filters.minRating && (
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium">
|
||||
Min Puan: ⭐{parseFloat(filters.minRating).toFixed(1)}
|
||||
<button onClick={() => updateFilter('minRating', '')} className="hover:text-blue-900">×</button>
|
||||
<button onClick={() => updateFilter('minRating', '')} className="hover:text-orange-900">×</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -600,19 +599,19 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Product Grid */}
|
||||
{filteredProducts.length === 0 ? (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-12 text-center">
|
||||
<div className="text-slate-400 mb-4">
|
||||
<svg className="mx-auto h-16 w-16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">Sonuç Bulunamadı</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
<h3 className="text-xl font-semibold text-slate-900 mb-2">Sonuç Bulunamadı</h3>
|
||||
<p className="text-slate-400 mb-4">
|
||||
Aradığınız kriterlere uygun ürün bulunamadı. Lütfen filtrelerinizi değiştirin.
|
||||
</p>
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors"
|
||||
className="px-4 py-2 bg-orange-500 text-white rounded-xl hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
Tüm Filtreleri Temizle
|
||||
</button>
|
||||
@@ -620,53 +619,53 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
) : (
|
||||
<>
|
||||
{/* Products Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-slate-200">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
|
||||
Görsel
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
|
||||
Ürün Bilgisi
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
|
||||
Marka
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
|
||||
Kategori
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-left text-sm font-medium text-slate-400">
|
||||
Menşei
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
|
||||
Fiyat
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
|
||||
Satış
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
|
||||
Görüntülenme
|
||||
</th>
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-gray-500">
|
||||
<th scope="col" className="p-3 text-right text-sm font-medium text-slate-400">
|
||||
Yorum
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-white divide-y divide-slate-200">
|
||||
{paginatedProducts.map((product) => {
|
||||
const productUrl = product.url?.startsWith('http') ? product.url : `https://www.trendyol.com${product.url}`
|
||||
|
||||
return (
|
||||
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
|
||||
<tr key={product.id} className="hover:bg-orange-50/30 transition-colors">
|
||||
{/* Image */}
|
||||
<td className="p-3 whitespace-nowrap">
|
||||
<a
|
||||
href={productUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-16 h-16 bg-gray-100 rounded-xl overflow-hidden flex-shrink-0 hover:ring-2 hover:ring-orange-500 transition-all"
|
||||
className="block w-16 h-16 bg-slate-100 rounded-xl overflow-hidden flex-shrink-0 hover:ring-2 hover:ring-orange-500 transition-all"
|
||||
>
|
||||
{product.image_url ? (
|
||||
<img
|
||||
@@ -676,7 +675,7 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<div className="w-full h-full flex items-center justify-center text-slate-400">
|
||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
@@ -692,34 +691,34 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
href={productUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-gray-900 hover:text-orange-600 transition-colors mb-1 block"
|
||||
className="text-sm font-medium text-slate-900 hover:text-orange-600 transition-colors mb-1 block"
|
||||
title={product.name}
|
||||
>
|
||||
{product.name}
|
||||
</a>
|
||||
{product.barcode && (
|
||||
<div className="text-xs text-gray-400 mt-1">Barkod: {product.barcode}</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Barkod: {product.barcode}</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Brand */}
|
||||
<td className="p-3 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{product.brand || '-'}</div>
|
||||
<div className="text-sm text-slate-900">{product.brand || '-'}</div>
|
||||
</td>
|
||||
|
||||
{/* Category */}
|
||||
<td className="p-3 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{product.category_name || '-'}</div>
|
||||
<div className="text-sm text-slate-900">{product.category_name || '-'}</div>
|
||||
</td>
|
||||
|
||||
{/* Country */}
|
||||
<td className="p-3 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">
|
||||
<div className="text-sm text-slate-900">
|
||||
{product.country ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span>{product.country}</span>
|
||||
<span className="text-xs text-gray-500">({product.country_code})</span>
|
||||
<span className="text-xs text-slate-400">({product.country_code})</span>
|
||||
</span>
|
||||
) : '-'}
|
||||
</div>
|
||||
@@ -727,28 +726,28 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Price */}
|
||||
<td className="p-3 whitespace-nowrap text-right">
|
||||
<div className="text-sm font-semibold text-blue-600">
|
||||
<div className="text-sm font-semibold text-orange-500">
|
||||
₺{product.price?.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Orders */}
|
||||
<td className="p-3 whitespace-nowrap text-right">
|
||||
<div className="text-sm text-gray-900">
|
||||
<div className="text-sm text-slate-900">
|
||||
{(product.orders || 0).toLocaleString('tr-TR')}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Views */}
|
||||
<td className="p-3 whitespace-nowrap text-right">
|
||||
<div className="text-sm text-gray-900">
|
||||
<div className="text-sm text-slate-900">
|
||||
{(product.page_views || 0).toLocaleString('tr-TR')}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Reviews */}
|
||||
<td className="p-3 whitespace-nowrap text-right">
|
||||
<div className="text-sm text-gray-900">
|
||||
<div className="text-sm text-slate-900">
|
||||
{(product.reviews || product.rating_count || 0).toLocaleString('tr-TR')}
|
||||
</div>
|
||||
</td>
|
||||
@@ -762,23 +761,23 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-slate-500">
|
||||
Sayfa {currentPage} / {totalPages} (Toplam {filteredProducts.length.toLocaleString('tr-TR')} ürün)
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
««
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
‹ Önceki
|
||||
</button>
|
||||
@@ -803,8 +802,8 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`px-3 py-1.5 text-sm border rounded-xl transition-colors ${
|
||||
currentPage === pageNum
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'border-gray-300 hover:bg-gray-50'
|
||||
? 'bg-orange-500 text-white border-orange-500'
|
||||
: 'border-slate-300 hover:bg-orange-50/30'
|
||||
}`}
|
||||
>
|
||||
{pageNum}
|
||||
@@ -816,14 +815,14 @@ export default function ProductFinderTab({ allProducts }) {
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Sonraki ›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-sm border border-slate-300 rounded-xl hover:bg-orange-50/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
»»
|
||||
</button>
|
||||
|
||||
33
admin-panel/src/components/ui/KpiCard.jsx
Normal file
33
admin-panel/src/components/ui/KpiCard.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
42
admin-panel/src/components/ui/Layout.jsx
Normal file
42
admin-panel/src/components/ui/Layout.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
113
admin-panel/src/components/ui/Sidebar.jsx
Normal file
113
admin-panel/src/components/ui/Sidebar.jsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
85
admin-panel/src/components/ui/SkeletonLoader.jsx
Normal file
85
admin-panel/src/components/ui/SkeletonLoader.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
45
admin-panel/src/components/ui/TopBar.jsx
Normal file
45
admin-panel/src/components/ui/TopBar.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
50
admin-panel/src/constants/chartColors.js
Normal file
50
admin-panel/src/constants/chartColors.js
Normal 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',
|
||||
},
|
||||
}
|
||||
@@ -11,7 +11,9 @@ export const TAB_GROUPS = {
|
||||
{ id: 'origin', name: 'Menşei' },
|
||||
{ id: 'barcode', name: 'Barkod' },
|
||||
{ id: 'keyword', name: 'Keyword Aracı' },
|
||||
{ id: 'product-finder', name: 'Ürün Bulma' }
|
||||
{ id: 'product-finder', name: 'Ürün Bulma' },
|
||||
{ id: 'hidden-champions', name: 'Gizli Şampiyonlar' },
|
||||
{ id: 'opportunity', name: 'Fırsat Haritası' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,73 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: #f8fafc; /* slate-50 */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
/* Skeleton loading animation */
|
||||
@keyframes skeleton-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
animation: skeleton-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1; /* slate-300 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8; /* slate-400 */
|
||||
}
|
||||
|
||||
/* Hide scrollbar utility */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Page transition */
|
||||
.page-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.page-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 200ms ease-out, transform 200ms ease-out;
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
button, a, input, select, textarea {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
114
admin-panel/src/utils/exportUtils.js
Normal file
114
admin-panel/src/utils/exportUtils.js
Normal 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
34778
categories/Bebek_104158.json
Normal file
File diff suppressed because it is too large
Load Diff
33923
categories/Cilt_Bakımı_85.json
Normal file
33923
categories/Cilt_Bakımı_85.json
Normal file
File diff suppressed because it is too large
Load Diff
32903
categories/Giyim_82.json
Normal file
32903
categories/Giyim_82.json
Normal file
File diff suppressed because it is too large
Load Diff
199870
reports/enrich_1/social.json
199870
reports/enrich_1/social.json
File diff suppressed because it is too large
Load Diff
292708
reports/enrich_2/social.json
292708
reports/enrich_2/social.json
File diff suppressed because it is too large
Load Diff
388522
reports/enrich_3/social.json
388522
reports/enrich_3/social.json
File diff suppressed because it is too large
Load Diff
16
reports/mart_anne_&_cocuk_raporua_20260307_173209.json
Normal file
16
reports/mart_anne_&_cocuk_raporua_20260307_173209.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
reports/mart_kadin_raporu_20260307_170556.json
Normal file
16
reports/mart_kadin_raporu_20260307_170556.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
reports/mart_kozmetik_raporu_20260307_163635.json
Normal file
16
reports/mart_kozmetik_raporu_20260307_163635.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user