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
166 lines
7.4 KiB
React
166 lines
7.4 KiB
React
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>
|
||
);
|
||
};
|