183 lines
10 KiB
React
183 lines
10 KiB
React
import React, { useRef, useState, useEffect, useCallback, useTransition } from 'react';
|
||
import { useApp } from '../context/AppContext';
|
||
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone, Cpu, ShieldAlert, Globe2, Gavel, CreditCard, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
|
||
|
||
// Her sekmenin kendi rengi — pasifken ikon tonu, aktifken dolgu. Tailwind JIT
|
||
// için sınıf adları tam yazılıdır.
|
||
const RENK = {
|
||
list: { ikon: 'text-sky-600', aktif: 'border-sky-300 bg-sky-100 text-sky-900', hover: 'hover:bg-sky-50' },
|
||
bulten: { ikon: 'text-amber-600', aktif: 'border-amber-300 bg-amber-100 text-amber-900', hover: 'hover:bg-amber-50' },
|
||
sonuc: { ikon: 'text-lime-600', aktif: 'border-lime-300 bg-lime-100 text-lime-900', hover: 'hover:bg-lime-50' },
|
||
takvim: { ikon: 'text-rose-600', aktif: 'border-rose-300 bg-rose-100 text-rose-900', hover: 'hover:bg-rose-50' },
|
||
sozlesme: { ikon: 'text-emerald-600', aktif: 'border-emerald-300 bg-emerald-100 text-emerald-900', hover: 'hover:bg-emerald-50' },
|
||
kdv: { ikon: 'text-teal-600', aktif: 'border-teal-300 bg-teal-100 text-teal-900', hover: 'hover:bg-teal-50' },
|
||
sinir: { ikon: 'text-orange-600', aktif: 'border-orange-300 bg-orange-100 text-orange-900', hover: 'hover:bg-orange-50' },
|
||
fiyat: { ikon: 'text-green-600', aktif: 'border-green-300 bg-green-100 text-green-900', hover: 'hover:bg-green-50' },
|
||
kanban: { ikon: 'text-violet-600', aktif: 'border-violet-300 bg-violet-100 text-violet-900', hover: 'hover:bg-violet-50' },
|
||
rakip_radar: { ikon: 'text-red-600', aktif: 'border-red-300 bg-red-100 text-red-900', hover: 'hover:bg-red-50' },
|
||
dokuman_deposu: { ikon: 'text-indigo-600', aktif: 'border-indigo-300 bg-indigo-100 text-indigo-900', hover: 'hover:bg-indigo-50' },
|
||
analitik_dashboard: { ikon: 'text-cyan-600', aktif: 'border-cyan-300 bg-cyan-100 text-cyan-900', hover: 'hover:bg-cyan-50' },
|
||
kirmizi_bayrak: { ikon: 'text-rose-700', aktif: 'border-rose-400 bg-rose-100 text-rose-900', hover: 'hover:bg-rose-50' },
|
||
kik_kararlari: { ikon: 'text-stone-600', aktif: 'border-stone-400 bg-stone-200 text-stone-900', hover: 'hover:bg-stone-100' },
|
||
ted_eu: { ikon: 'text-blue-600', aktif: 'border-blue-300 bg-blue-100 text-blue-900', hover: 'hover:bg-blue-50' },
|
||
planlar: { ikon: 'text-pink-600', aktif: 'border-pink-300 bg-pink-100 text-pink-900', hover: 'hover:bg-pink-50' },
|
||
api_webhooks: { ikon: 'text-slate-600', aktif: 'border-slate-400 bg-slate-200 text-slate-900', hover: 'hover:bg-slate-100' },
|
||
ai_ayarlari: { ikon: 'text-purple-600', aktif: 'border-purple-300 bg-purple-100 text-purple-900', hover: 'hover:bg-purple-50' },
|
||
pwa_settings: { ikon: 'text-fuchsia-600', aktif: 'border-fuchsia-300 bg-fuchsia-100 text-fuchsia-900', hover: 'hover:bg-fuchsia-50' },
|
||
};
|
||
const RENK_VARSAYILAN = { ikon: 'text-slate-500', aktif: 'border-blue-200 bg-blue-50 text-blue-700', hover: 'hover:bg-slate-50' };
|
||
|
||
const NAV_GROUPS = [
|
||
{id:'discover',label:'Keşfet',tabs:['list','bulten','takvim','ted_eu']},
|
||
{id:'prepare',label:'Hazırlan',tabs:['kanban','dokuman_deposu','kdv','sinir','kik_kararlari']},
|
||
{id:'market',label:'Pazar',tabs:['sonuc','sozlesme','fiyat','rakip_radar','analitik_dashboard','kirmizi_bayrak']},
|
||
{id:'manage',label:'Yönetim',tabs:['planlar','api_webhooks','pwa_settings','ai_ayarlari']},
|
||
];
|
||
|
||
export const TopNavigation = () => {
|
||
const { activeTab, setActiveTab, isAdmin } = useApp();
|
||
|
||
// Ağır sekmeler (10 bin+ kayıtlı havuzda Kanban, analiz vb.) tıklamadan
|
||
// sonra saniyelerce hiçbir tepki vermiyordu — kullanıcı "tıklanmıyor" sanıyordu.
|
||
// Sekme değişimi geçiş (transition) olarak yapılır: tıklanan sekme ANINDA
|
||
// vurgulanır ve dönen simge gösterilir, ağır görünüm arka planda hazırlanır.
|
||
const [isPending, startTransition] = useTransition();
|
||
const [hedefTab, setHedefTab] = useState(null);
|
||
const sekmeSec = (id) => {
|
||
if (id === activeTab) return;
|
||
setHedefTab(id);
|
||
startTransition(() => setActiveTab(id));
|
||
};
|
||
const bekleyen = isPending ? hedefTab : null;
|
||
|
||
const navItems = [
|
||
{ id: 'list', label: 'Ana İhale Listesi', icon: LayoutGrid },
|
||
{ id: 'bulten', label: 'Günlük Bülten', icon: Newspaper },
|
||
{ id: 'sonuc', label: 'Sonuç Sorgula', icon: Trophy },
|
||
{ id: 'takvim', label: 'İhale Takvimi', icon: CalendarDays },
|
||
{ id: 'sozlesme', label: 'Sözleşme Sorgula', icon: FileCheck2 },
|
||
{ id: 'kdv', label: 'KDV Hesapla', icon: Calculator },
|
||
{ id: 'sinir', label: 'Sınır Değer', icon: Calculator },
|
||
{ id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp },
|
||
{ id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' },
|
||
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar },
|
||
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
|
||
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
|
||
{ id: 'kirmizi_bayrak', label: 'Kırmızı Bayrak', icon: ShieldAlert, badge: 'Şeffaflık' },
|
||
{ id: 'kik_kararlari', label: 'KİK Kurul Kararları', icon: Gavel },
|
||
{ id: 'ted_eu', label: 'AB İhaleleri (TED)', icon: Globe2 },
|
||
{ id: 'planlar', label: 'Planlar', icon: CreditCard },
|
||
{ id: 'api_webhooks', label: 'API & Webhooks', icon: Webhook },
|
||
// Sadece admin: AI Model Ayarları
|
||
...(isAdmin ? [{ id: 'ai_ayarlari', label: 'AI Model Ayarları', icon: Cpu, badge: 'Admin' }] : []),
|
||
{ id: 'pwa_settings', label: 'Mobil PWA', icon: Smartphone }
|
||
];
|
||
|
||
const activeGroup=NAV_GROUPS.find(group=>group.tabs.includes(bekleyen || activeTab)) || NAV_GROUPS[0];
|
||
const visibleItems=activeGroup.tabs.map(id=>navItems.find(item=>item.id===id)).filter(Boolean);
|
||
|
||
// 18 sekme çoğu ekrana sığmıyor. Kaydırma çubuğu gizli olduğu için sondaki
|
||
// sekmeler (Planlar, API & Webhooks, Mobil PWA) görünmez kalıyordu; taşma
|
||
// olduğunda ok düğmeleri gösterilir ve etkin sekme görünür alana kaydırılır.
|
||
const seritRef = useRef(null);
|
||
const [tasma, setTasma] = useState({ sol: false, sag: false });
|
||
|
||
const tasmaHesapla = useCallback(() => {
|
||
const el = seritRef.current;
|
||
if (!el) return;
|
||
setTasma({
|
||
sol: el.scrollLeft > 4,
|
||
sag: el.scrollLeft + el.clientWidth < el.scrollWidth - 4,
|
||
});
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
tasmaHesapla();
|
||
const el = seritRef.current;
|
||
if (!el) return;
|
||
el.addEventListener('scroll', tasmaHesapla, { passive: true });
|
||
window.addEventListener('resize', tasmaHesapla);
|
||
return () => {
|
||
el.removeEventListener('scroll', tasmaHesapla);
|
||
window.removeEventListener('resize', tasmaHesapla);
|
||
};
|
||
}, [tasmaHesapla]);
|
||
|
||
// Etkin sekme (ör. menüden 'Planlar' seçilince) görünür alana getirilir.
|
||
useEffect(() => {
|
||
const el = seritRef.current?.querySelector(`[data-tab="${activeTab}"]`);
|
||
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||
tasmaHesapla();
|
||
}, [activeTab, activeGroup.id, tasmaHesapla]);
|
||
|
||
const kaydir = (yon) => {
|
||
seritRef.current?.scrollBy({ left: yon * 280, behavior: 'smooth' });
|
||
};
|
||
|
||
return (
|
||
<div className="bg-white border-b border-slate-200 shadow-xs sticky top-16 z-30">
|
||
<nav aria-label="Çalışma alanları" className="max-w-[1600px] mx-auto px-4 pt-2 flex flex-wrap gap-2">
|
||
{NAV_GROUPS.map(group=><button key={group.id} type="button" aria-pressed={group.id===activeGroup.id}
|
||
onClick={()=>{if(group.id!==activeGroup.id)sekmeSec(group.tabs.find(id=>navItems.some(item=>item.id===id)));}}
|
||
className={`px-4 py-2 rounded-lg text-sm font-bold border ${group.id===activeGroup.id ? 'bg-slate-900 text-white border-slate-900' : 'border-slate-200 text-slate-700 hover:bg-slate-50'}`}>
|
||
{group.label}
|
||
</button>)}
|
||
</nav>
|
||
<div className="max-w-[1600px] mx-auto px-4 relative">
|
||
{tasma.sol && (
|
||
<button
|
||
type="button"
|
||
onClick={() => kaydir(-1)}
|
||
aria-label="Önceki sekmeler"
|
||
className="absolute left-1 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white border border-slate-200 shadow-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-50"
|
||
>
|
||
<ChevronLeft className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
{tasma.sag && (
|
||
<button
|
||
type="button"
|
||
onClick={() => kaydir(1)}
|
||
aria-label="Sonraki sekmeler"
|
||
className="absolute right-1 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white border border-slate-200 shadow-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-50"
|
||
>
|
||
<ChevronRight className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
<nav aria-label={`${activeGroup.label} araçları`} ref={seritRef} className="flex items-center gap-1 overflow-x-auto py-1 scrollbar-none scroll-smooth">
|
||
{visibleItems.map(item => {
|
||
const Icon = item.icon;
|
||
const renk = RENK[item.id] || RENK_VARSAYILAN;
|
||
const isActive = bekleyen ? bekleyen === item.id : activeTab === item.id;
|
||
const yukleniyor = bekleyen === item.id;
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
data-tab={item.id}
|
||
aria-current={isActive ? 'page' : undefined}
|
||
aria-busy={yukleniyor || undefined}
|
||
onClick={() => sekmeSec(item.id)}
|
||
className={`flex items-center gap-2 px-3.5 py-2 rounded-lg border text-xs font-semibold whitespace-nowrap transition-all select-none ${
|
||
isActive
|
||
? `${renk.aktif} shadow-2xs`
|
||
: `border-transparent text-slate-700 hover:text-slate-900 ${renk.hover}`
|
||
}`}
|
||
>
|
||
{yukleniyor
|
||
? <Loader2 className={`w-4 h-4 animate-spin ${renk.ikon}`} />
|
||
: <Icon className={`w-4 h-4 ${renk.ikon}`} />}
|
||
<span>{item.label}</span>
|
||
{item.badge && (
|
||
<span className="bg-amber-100 text-amber-800 font-bold text-[10px] px-1.5 py-0.5 rounded-full border border-amber-200">
|
||
{item.badge}
|
||
</span>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|