fix(admin): Consistent navigation across Intelligence pages
- Add state selector dropdown to all three Intelligence pages (Brands, Stores, Pricing) - Use consistent emerald-styled page navigation badges with current page highlighted - Remove Refresh buttons from all Intelligence pages - Update chart styling to use emerald gradient bars (matching Pricing page) - Load all available states from orchestrator API instead of extracting from local data - Fix z-index and styling on state dropdown for better visibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,6 @@ import {
|
|||||||
MapPin,
|
MapPin,
|
||||||
Package,
|
Package,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
RefreshCw,
|
|
||||||
Search,
|
Search,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
@@ -100,37 +99,62 @@ export function IntelligenceBrands() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Brands Intelligence</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Brands Intelligence</h1>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
Brand penetration and pricing analytics across markets
|
Brand penetration and pricing analytics across markets
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
|
{/* State Selector */}
|
||||||
|
<div className="dropdown dropdown-end">
|
||||||
|
<button tabIndex={0} className="btn btn-sm gap-2 bg-emerald-50 border-emerald-200 hover:bg-emerald-100">
|
||||||
|
{stateLabel}
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<ul tabIndex={0} className="dropdown-content z-50 menu p-2 shadow-lg bg-white rounded-box w-44 max-h-60 overflow-y-auto border border-gray-200">
|
||||||
|
<li>
|
||||||
|
<a onClick={() => setSelectedState(null)} className={isAllStates ? 'active bg-emerald-100' : ''}>
|
||||||
|
All States
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<div className="divider my-1"></div>
|
||||||
|
{availableStates.map((state) => (
|
||||||
|
<li key={state}>
|
||||||
|
<a onClick={() => setSelectedState(state)} className={selectedState === state ? 'active bg-emerald-100' : ''}>
|
||||||
|
{state}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page Navigation */}
|
||||||
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin/intelligence/pricing')}
|
className="btn btn-sm gap-1 bg-emerald-600 text-white hover:bg-emerald-700 border-emerald-600"
|
||||||
className="btn btn-sm btn-outline gap-1"
|
|
||||||
>
|
>
|
||||||
<DollarSign className="w-4 h-4" />
|
<Building2 className="w-4 h-4" />
|
||||||
Pricing
|
<span>Brands</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin/intelligence/stores')}
|
onClick={() => navigate('/admin/intelligence/stores')}
|
||||||
className="btn btn-sm btn-outline gap-1"
|
className="btn btn-sm btn-outline gap-1"
|
||||||
>
|
>
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
Stores
|
<span>Stores</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={loadBrands}
|
onClick={() => navigate('/admin/intelligence/pricing')}
|
||||||
className="btn btn-sm btn-outline gap-2"
|
className="btn btn-sm btn-outline gap-1"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<DollarSign className="w-4 h-4" />
|
||||||
Refresh
|
<span>Pricing</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
@@ -180,51 +204,32 @@ export function IntelligenceBrands() {
|
|||||||
|
|
||||||
{/* Top Brands Chart */}
|
{/* Top Brands Chart */}
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2 mb-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
<BarChart3 className="w-5 h-5 text-emerald-500" />
|
||||||
<BarChart3 className="w-5 h-5 text-blue-500" />
|
|
||||||
Top 10 Brands by Store Count
|
Top 10 Brands by Store Count
|
||||||
</h3>
|
</h3>
|
||||||
<div className="dropdown dropdown-end">
|
|
||||||
<button tabIndex={0} className="btn btn-sm btn-outline gap-2">
|
|
||||||
{stateLabel}
|
|
||||||
<ChevronDown className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-40 max-h-60 overflow-y-auto">
|
|
||||||
<li>
|
|
||||||
<a onClick={() => setSelectedState(null)} className={isAllStates ? 'active' : ''}>
|
|
||||||
All States
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="divider"></li>
|
|
||||||
{availableStates.map((state) => (
|
|
||||||
<li key={state}>
|
|
||||||
<a onClick={() => setSelectedState(state)} className={selectedState === state ? 'active' : ''}>
|
|
||||||
{state}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{topBrands.map((brand, idx) => (
|
{topBrands.map((brand) => {
|
||||||
|
const barWidth = Math.min((brand.storeCount / maxStoreCount) * 100, 100);
|
||||||
|
return (
|
||||||
<div key={brand.brandName} className="flex items-center gap-3">
|
<div key={brand.brandName} className="flex items-center gap-3">
|
||||||
<span className="text-sm text-gray-500 w-6">{idx + 1}.</span>
|
<span className="text-sm font-medium w-28 truncate shrink-0" title={brand.brandName}>
|
||||||
<span className="text-sm font-medium w-40 truncate" title={brand.brandName}>
|
|
||||||
{brand.brandName}
|
{brand.brandName}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 bg-gray-100 rounded-full h-4 relative">
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="bg-gray-100 rounded h-5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-500 rounded-full h-4"
|
className="bg-gradient-to-r from-emerald-400 to-emerald-500 h-5 rounded transition-all"
|
||||||
style={{ width: `${(brand.storeCount / maxStoreCount) * 100}%` }}
|
style={{ width: `${barWidth}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-600 w-16 text-right">
|
</div>
|
||||||
{brand.storeCount} stores
|
<span className="text-sm font-mono font-semibold text-emerald-600 w-16 text-right shrink-0">
|
||||||
|
{brand.storeCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Building2,
|
Building2,
|
||||||
MapPin,
|
MapPin,
|
||||||
Package,
|
Package,
|
||||||
RefreshCw,
|
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
@@ -87,58 +86,62 @@ export function IntelligencePricing() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Pricing Intelligence</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Pricing Intelligence</h1>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
Price distribution and trends by category
|
Price distribution and trends by category
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
|
{/* State Selector */}
|
||||||
<div className="dropdown dropdown-end">
|
<div className="dropdown dropdown-end">
|
||||||
<button tabIndex={0} className="btn btn-sm btn-outline gap-2">
|
<button tabIndex={0} className="btn btn-sm gap-2 bg-emerald-50 border-emerald-200 hover:bg-emerald-100">
|
||||||
{stateLabel}
|
{stateLabel}
|
||||||
<ChevronDown className="w-4 h-4" />
|
<ChevronDown className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-40 max-h-60 overflow-y-auto">
|
<ul tabIndex={0} className="dropdown-content z-50 menu p-2 shadow-lg bg-white rounded-box w-44 max-h-60 overflow-y-auto border border-gray-200">
|
||||||
<li>
|
<li>
|
||||||
<a onClick={() => setSelectedState(null)} className={isAllStates ? 'active' : ''}>
|
<a onClick={() => setSelectedState(null)} className={isAllStates ? 'active bg-emerald-100' : ''}>
|
||||||
All States
|
All States
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="divider"></li>
|
<div className="divider my-1"></div>
|
||||||
{availableStates.map((state) => (
|
{availableStates.map((state) => (
|
||||||
<li key={state}>
|
<li key={state}>
|
||||||
<a onClick={() => setSelectedState(state)} className={selectedState === state ? 'active' : ''}>
|
<a onClick={() => setSelectedState(state)} className={selectedState === state ? 'active bg-emerald-100' : ''}>
|
||||||
{state}
|
{state}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Page Navigation */}
|
||||||
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin/intelligence/brands')}
|
onClick={() => navigate('/admin/intelligence/brands')}
|
||||||
className="btn btn-sm btn-outline gap-1"
|
className="btn btn-sm btn-outline gap-1"
|
||||||
>
|
>
|
||||||
<Building2 className="w-4 h-4" />
|
<Building2 className="w-4 h-4" />
|
||||||
Brands
|
<span>Brands</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin/intelligence/stores')}
|
onClick={() => navigate('/admin/intelligence/stores')}
|
||||||
className="btn btn-sm btn-outline gap-1"
|
className="btn btn-sm btn-outline gap-1"
|
||||||
>
|
>
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
Stores
|
<span>Stores</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={loadPricing}
|
className="btn btn-sm gap-1 bg-emerald-600 text-white hover:bg-emerald-700 border-emerald-600"
|
||||||
className="btn btn-sm btn-outline gap-2"
|
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<DollarSign className="w-4 h-4" />
|
||||||
Refresh
|
<span>Pricing</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Overall Stats */}
|
{/* Overall Stats */}
|
||||||
{overall && (
|
{overall && (
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Building2,
|
Building2,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Package,
|
Package,
|
||||||
RefreshCw,
|
|
||||||
Search,
|
Search,
|
||||||
Clock,
|
Clock,
|
||||||
Activity,
|
Activity,
|
||||||
@@ -34,12 +33,19 @@ export function IntelligenceStores() {
|
|||||||
const [stores, setStores] = useState<StoreActivity[]>([]);
|
const [stores, setStores] = useState<StoreActivity[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [localStates, setLocalStates] = useState<string[]>([]);
|
const [availableStates, setAvailableStates] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStores();
|
loadStores();
|
||||||
}, [selectedState]);
|
}, [selectedState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Load available states from orchestrator API
|
||||||
|
api.getOrchestratorStates().then(data => {
|
||||||
|
setAvailableStates(data.states?.map((s: any) => s.state) || []);
|
||||||
|
}).catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadStores = async () => {
|
const loadStores = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -47,12 +53,7 @@ export function IntelligenceStores() {
|
|||||||
state: stateParam,
|
state: stateParam,
|
||||||
limit: 500,
|
limit: 500,
|
||||||
});
|
});
|
||||||
const storeList = data.stores || [];
|
setStores(data.stores || []);
|
||||||
setStores(storeList);
|
|
||||||
|
|
||||||
// Extract unique states from response for dropdown counts
|
|
||||||
const uniqueStates = [...new Set(storeList.map((s: StoreActivity) => s.state))].filter(Boolean).sort() as string[];
|
|
||||||
setLocalStates(uniqueStates);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load stores:', error);
|
console.error('Failed to load stores:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -110,37 +111,62 @@ export function IntelligenceStores() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Store Activity</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Store Activity</h1>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
Per-store SKU counts, snapshots, and crawl frequency
|
Per-store SKU counts, snapshots, and crawl frequency
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
|
{/* State Selector */}
|
||||||
|
<div className="dropdown dropdown-end">
|
||||||
|
<button tabIndex={0} className="btn btn-sm gap-2 bg-emerald-50 border-emerald-200 hover:bg-emerald-100">
|
||||||
|
{stateLabel}
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<ul tabIndex={0} className="dropdown-content z-50 menu p-2 shadow-lg bg-white rounded-box w-44 max-h-60 overflow-y-auto border border-gray-200">
|
||||||
|
<li>
|
||||||
|
<a onClick={() => setSelectedState(null)} className={isAllStates ? 'active bg-emerald-100' : ''}>
|
||||||
|
All States
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<div className="divider my-1"></div>
|
||||||
|
{availableStates.map((state) => (
|
||||||
|
<li key={state}>
|
||||||
|
<a onClick={() => setSelectedState(state)} className={selectedState === state ? 'active bg-emerald-100' : ''}>
|
||||||
|
{state}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page Navigation */}
|
||||||
|
<div className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin/intelligence/brands')}
|
onClick={() => navigate('/admin/intelligence/brands')}
|
||||||
className="btn btn-sm btn-outline gap-1"
|
className="btn btn-sm btn-outline gap-1"
|
||||||
>
|
>
|
||||||
<Building2 className="w-4 h-4" />
|
<Building2 className="w-4 h-4" />
|
||||||
Brands
|
<span>Brands</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm gap-1 bg-emerald-600 text-white hover:bg-emerald-700 border-emerald-600"
|
||||||
|
>
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
<span>Stores</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin/intelligence/pricing')}
|
onClick={() => navigate('/admin/intelligence/pricing')}
|
||||||
className="btn btn-sm btn-outline gap-1"
|
className="btn btn-sm btn-outline gap-1"
|
||||||
>
|
>
|
||||||
<DollarSign className="w-4 h-4" />
|
<DollarSign className="w-4 h-4" />
|
||||||
Pricing
|
<span>Pricing</span>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={loadStores}
|
|
||||||
className="btn btn-sm btn-outline gap-2"
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-4 h-4" />
|
|
||||||
Refresh
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards - Responsive: 2→4 columns */}
|
{/* Summary Cards - Responsive: 2→4 columns */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user