Formuller kaynagindan alindi (mevzuat.gov.tr Kamu Ihale Genel Tebligi, kullanicinin Chrome'u uzerinden okundu; formuller Teblig'de gorsel oldugu icin ekran goruntusunden): - Md. 79.1.2 hizmet (personel dayali olmayan): SD = ((YM + ΣT)/(n+1)) × R, YM'nin %60 alti ve YM ustu teklifler haric - Md. 79.1.1 personel calistirmaya dayali: SD = kar haric YM - Md. 62/A.1 (RG 9/4/2026): mal aliminda 79 uygulanir - Md. 45.1.1 yapim: Tort1/σ/Tort2, C = Tort2/YM, K parcali, SD = K×Tort2/(C×N); N 1,00 (B/C/D/E gruplari) veya 1,20 (45.1.1.1) - R katsayilari: KIK Kurul karari 21.01.2026 (.doc'tan okundu): arac kiralama 0,77, bilgisayar 0,87, yemek 0,77, muhendislik 0,72, sigorta 0,74, tasima 0,78, diger 0,78 Dogrulama: Teblig'in kendi 45.2 ornegi (YM 100.000, N 1,20, 12 teklif) ile birebir — Tort1 66.270,00, σ 20.317,05, Tort2 63.237,50, C 0,632, K 0,627, SD 52.281,00. Yuvarlama kurallari ornekteki gibi (2/3 ondalik). Arac: ust menude "Sinir Deger"; alim turu, YM, teklif listesi, R/N secimi, "benim teklifim sinirin altinda mi" uyarisi, adim adim hesap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
138 lines
6.3 KiB
React
138 lines
6.3 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';
|
||
|
||
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, badge: 'Canlı Uyar' },
|
||
{ 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 }
|
||
];
|
||
|
||
// 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' });
|
||
}, [activeTab]);
|
||
|
||
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">
|
||
<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>
|
||
)}
|
||
<div ref={seritRef} className="flex items-center gap-1 overflow-x-auto py-1 scrollbar-none scroll-smooth">
|
||
{navItems.map(item => {
|
||
const Icon = item.icon;
|
||
const isActive = bekleyen ? bekleyen === item.id : activeTab === item.id;
|
||
const yukleniyor = bekleyen === item.id;
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
data-tab={item.id}
|
||
aria-busy={yukleniyor || undefined}
|
||
onClick={() => sekmeSec(item.id)}
|
||
className={`flex items-center gap-2 px-3.5 py-2 rounded-lg text-xs font-semibold whitespace-nowrap transition-all select-none ${
|
||
isActive
|
||
? 'bg-blue-50 text-blue-700 border border-blue-200 shadow-2xs'
|
||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
|
||
}`}
|
||
>
|
||
{yukleniyor
|
||
? <Loader2 className="w-4 h-4 text-blue-600 animate-spin" />
|
||
: <Icon className={`w-4 h-4 ${isActive ? 'text-blue-600' : 'text-slate-400'}`} />}
|
||
<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>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|