Files
ihaleist/src/components/tools/KikDecisions.jsx
T
hikmettan 168052c267 feat: geçmiş veri motoru + gerçek AI + KİK kararları + TED + kırmızı bayrak + planlar
Roadmap'in 6 maddesi de gerçek çalışır halde:

1) Geçmiş veri motoru (EKAP v2)
   - server/ekap-v2.mjs: AES-256-CBC imzalı EKAP v2 istemcisi (zero-dep),
     ihale arama + sonuç ilanı parse (kazanan firma, sözleşme bedeli,
     yaklaşık maliyet, teklif sayısı), TR firma adı normalize
   - server/history-store.mjs: SQLite deposu (IKN+kısım anahtarlı, upsert)
   - server/ekap-scan.mjs: tarih aralıklı artımlı tarayıcı (6 paralel)
   - server/scheduler.mjs: sunucu içi periyodik tarama (SCAN_* env ile)
   - /api/tenders artık geçmiş sonuçları da birleştiriyor → kazanan firma,
     rakip ve kırmızı bayrak analizleri gerçek veriyle doluyor

2) Gerçek AI şartname analizi
   - server/ai.mjs: 6 sağlayıcılı fallback zinciri (GLM/DeepSeek/Gemini/
     Grok/GPT/Claude), JSON şemalı çıktı, uydurma yasak promptu
   - pdfjs-dist ile tarayıcıda PDF metin çıkarma (dinamik import)
   - AiSpecAnalyzerModal: sahte sonuç kaldırıldı, gerçek dosya + dürüst hata

3) KİK kurul kararları
   - server/kik.mjs: EKAP v2 JSON API (Sec-1..6 imzalı), uyuşmazlık/
     düzenleyici/mahkeme; karar tam metni (ayrı AES anahtarıyla URL)
   - KikDecisions sekmesi: arama + takipteki idarelerden hızlı öneri

4) Fiyatlandırma + ödeme
   - server/billing.mjs: Stripe Checkout (zero-dep, form-encoded)
   - Pricing sekmesi: 3 plan, aylık/yıllık, yapılandırılmadıysa dürüst uyarı

5) TED (AB ihaleleri)
   - server/ted.mjs: anonim TED v3 API, 3 mod (açık/tüm TR/AB'de kazanan TR)
   - rate-limit korumalı (429 HTML gövdesi ele alınır), çok dilli alan çözümü

6) Kırmızı bayrak & şeffaflık
   - analytics.js: redFlags (tek teklif, düşük kırım, tahmin üstü bedel,
     aşırı düşük teklif, tedarikçi yoğunlaşması) + marketConcentration (HHI)
   - RedFlags sekmesi: seviyeli uyarılar + yöntem şeffaflığı notu
2026-08-28 10:37:50 +03:00

166 lines
7.4 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, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Gavel, Search, Loader2, ExternalLink, AlertTriangle, Calendar, Building2, Info } from 'lucide-react';
export const KikDecisions = () => {
const { tenders } = useApp();
const [q, setQ] = useState('');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const ara = async (terim) => {
const t = (terim ?? q).trim();
if (!t) return;
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/kik/search?q=${encodeURIComponent(t)}&limit=20`);
const d = await res.json();
if (d.success) setData(d);
else { setError(d.error || 'Arama başarısız'); setData(null); }
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
// Takipteki ihalelerin idarelerinden hızlı arama önerileri
const oneriler = [...new Set(
tenders.filter((t) => t.takipte || t.kanbanStatus).map((t) => t.kurum).filter(Boolean)
)].slice(0, 4);
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-linear-to-r from-slate-900 via-amber-950 to-orange-900 text-white p-6 rounded-2xl shadow-lg border border-amber-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-amber-600 flex items-center justify-center shadow-md">
<Gavel className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">KİK Kurul Kararları</h2>
<p className="text-xs text-amber-200 mt-0.5">
Kamu İhale Kurulu itirazen şikâyet kararlarında arama yapın; emsal kararları teklif öncesi inceleyin.
</p>
</div>
</div>
</div>
{/* Arama */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
value={q}
onChange={(e) => setQ(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && ara()}
placeholder="Karar metni, idare adı veya başvuran firma…"
className="w-full bg-slate-50 border border-slate-300 rounded-lg pl-9 pr-4 py-2.5 text-xs font-semibold"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
</div>
<button
onClick={() => ara()}
disabled={loading || !q.trim()}
className="bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white font-bold text-xs px-5 rounded-lg flex items-center gap-2"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />} Ara
</button>
</div>
{oneriler.length > 0 && (
<div className="flex gap-2 flex-wrap items-center">
<span className="text-[11px] text-slate-400 font-semibold">Takibinizdeki idareler:</span>
{oneriler.map((k) => (
<button key={k} onClick={() => { setQ(k); ara(k); }}
className="text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 px-2 py-1 rounded-md font-medium truncate max-w-[220px]">
{k}
</button>
))}
</div>
)}
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex gap-3">
<AlertTriangle className="w-4 h-4 text-red-600 shrink-0 mt-0.5" />
<div>
<p className="text-xs font-bold text-red-900">KİK kararlarına ulaşılamadı</p>
<p className="text-xs text-red-800 mt-0.5 leading-relaxed">{error}</p>
<p className="text-[11px] text-red-700 mt-1">
KİK sunucusu zaman zaman otomatik isteklere kapalı olabilir; birkaç dakika sonra tekrar deneyin.
</p>
</div>
</div>
)}
{data && (
<>
<div className="flex items-center justify-between gap-3 flex-wrap">
<p className="text-xs text-slate-500">
<strong className="text-slate-900">{data.totalCount?.toLocaleString('tr-TR') ?? data.results.length}</strong> karar bulundu
</p>
{data.kaynak && <span className="text-[11px] text-slate-400">Kaynak: {data.kaynak}</span>}
</div>
<div className="space-y-3">
{data.results.length === 0 && (
<div className="bg-white rounded-2xl border border-dashed border-slate-300 p-10 text-center">
<p className="text-sm font-bold text-slate-500">Bu aramada karar bulunamadı</p>
</div>
)}
{data.results.map((k, i) => (
<div key={k.id || i} className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs hover:shadow-md transition-shadow space-y-2">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
{k.kararNo && (
<span className="text-[10px] font-extrabold bg-amber-100 text-amber-900 px-2 py-0.5 rounded">
{k.kararNo}
</span>
)}
{k.kararTarihi && (
<span className="text-[11px] text-slate-500 flex items-center gap-1">
<Calendar className="w-3 h-3" /> {k.kararTarihi}
</span>
)}
{k.tur && <span className="text-[10px] bg-slate-100 text-slate-700 px-2 py-0.5 rounded font-bold">{k.tur}</span>}
</div>
<h3 className="font-bold text-sm text-slate-900 mt-1.5 leading-snug">{k.baslik || k.konu || 'Kurul Kararı'}</h3>
</div>
{k.link && (
<a href={k.link} target="_blank" rel="noopener noreferrer" className="text-amber-700 hover:text-amber-900 shrink-0" title="Kararı aç">
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
{(k.idare || k.basvuran) && (
<div className="flex items-center gap-3 flex-wrap text-[11px] text-slate-600">
{k.idare && <span className="flex items-center gap-1"><Building2 className="w-3 h-3 text-slate-400" /> {k.idare}</span>}
{k.basvuran && <span>Başvuran: <strong>{k.basvuran}</strong></span>}
</div>
)}
{k.ozet && <p className="text-xs text-slate-600 leading-relaxed line-clamp-3">{k.ozet}</p>}
</div>
))}
</div>
</>
)}
{!data && !loading && !error && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 flex gap-3">
<Info className="w-4 h-4 text-blue-600 shrink-0 mt-0.5" />
<p className="text-xs text-blue-900 leading-relaxed">
Bir idare adı, firma veya konu yazıp arayın. KİK kurul kararları, benzer ihalelerde
hangi itirazların kabul/ret edildiğini gösterir — teklif ve şikâyet stratejinizi buna göre kurabilirsiniz.
</p>
</div>
)}
</div>
);
};