Files
ihaleist/src/components/TopNavigation.jsx
T
hikmettanandClaude Fable 5.1 91b8ea94b8 feat: sinir deger hesaplayici — Tebliğ md. 79/45 formulleri, 2026 R katsayilari
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>
2026-09-04 00:16:58 +03:00

138 lines
6.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};