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
This commit is contained in:
2026-08-28 10:37:50 +03:00
parent 3e129c2988
commit 168052c267
26 changed files with 3121 additions and 219 deletions
+153
View File
@@ -0,0 +1,153 @@
import React, { useState, useEffect } from 'react';
import { Globe2, Loader2, ExternalLink, Building2, Calendar, Trophy, AlertTriangle, RefreshCw } from 'lucide-react';
const MODLAR = [
{ id: 'acik', label: 'Açık AB İhaleleri', aciklama: 'Türkiye’deki idarelerin AB’de yayımladığı, hâlen teklif verilebilir ihaleler.' },
{ id: 'turkiye', label: 'Tüm Türkiye Kayıtları', aciklama: 'TR idareler + TR kazananlar + TR teklif verenler.' },
{ id: 'kazanan', label: 'AB’de Kazanan TR Firmaları', aciklama: 'Türk firmalarının AB ihalelerinde kazandığı sözleşmeler.' },
];
export const TedTenders = () => {
const [mod, setMod] = useState('acik');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const yukle = (m) => {
setLoading(true);
setError(null);
fetch(`/api/ted?mod=${m}&limit=30`)
.then((r) => r.json())
.then((d) => { if (d.success) setData(d); else setError(d.error); })
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
};
useEffect(() => { yukle(mod); }, [mod]);
const aktifMod = MODLAR.find((m) => m.id === mod);
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-sky-900 text-white p-6 rounded-2xl shadow-lg border border-sky-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-sky-600 flex items-center justify-center shadow-md">
<Globe2 className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">AB İhaleleri — TED</h2>
<p className="text-xs text-sky-200 mt-0.5">
Tenders Electronic Daily (AB resmi ihale yayını) üzerinden Türkiye ile ilgili ihaleler.
</p>
</div>
</div>
</div>
{/* Mod seçici */}
<div className="flex gap-2 flex-wrap">
{MODLAR.map((m) => (
<button
key={m.id}
onClick={() => setMod(m.id)}
className={`px-4 py-2 rounded-xl text-xs font-bold border transition-colors ${
mod === m.id ? 'bg-sky-600 text-white border-sky-600 shadow-xs' : 'bg-white text-slate-700 border-slate-200 hover:bg-slate-50'
}`}
>
{m.label}
</button>
))}
<button
onClick={() => yukle(mod)}
disabled={loading}
className="px-3 py-2 rounded-xl text-xs font-bold border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 flex items-center gap-1.5 disabled:opacity-50"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} /> Yenile
</button>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 flex items-center justify-between gap-3 flex-wrap shadow-xs">
<p className="text-xs text-slate-600">{aktifMod?.aciklama}</p>
{data && (
<span className="text-xs font-bold text-sky-800 bg-sky-50 border border-sky-200 px-3 py-1 rounded-full shrink-0">
{data.totalCount.toLocaleString('tr-TR')} kayıt
</span>
)}
</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">TED verisi alınamadı</p>
<p className="text-xs text-red-800 mt-0.5">{error}</p>
</div>
</div>
)}
{loading && !data && (
<div className="bg-white rounded-2xl border border-slate-200 p-10 text-center">
<Loader2 className="w-6 h-6 text-sky-600 animate-spin mx-auto" />
<p className="text-xs text-slate-500 mt-3">TED'den çekiliyor…</p>
</div>
)}
{data && (
<div className="space-y-3">
{data.tenders.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 filtrede kayıt bulunamadı</p>
</div>
)}
{data.tenders.map((t) => (
<div key={t.id} 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">
<h3 className="font-bold text-sm text-slate-900 leading-snug">{t.baslik}</h3>
{t.link && (
<a href={t.link} target="_blank" rel="noopener noreferrer"
className="text-sky-600 hover:text-sky-800 shrink-0" title="TED'de aç">
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
<div className="flex items-center gap-3 flex-wrap text-[11px] text-slate-600">
<span className="flex items-center gap-1 font-semibold">
<Building2 className="w-3 h-3 text-slate-400" /> {t.kurum}
</span>
{t.sehir && <span>{t.sehir}</span>}
{t.ulke && <span className="bg-slate-100 px-1.5 py-0.5 rounded font-bold">{t.ulke}</span>}
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3 text-blue-500" />
{t.sonTeklifTarihi ? `Son teklif: ${t.sonTeklifTarihi}` : t.yayimTarihi}
</span>
<span className="bg-sky-50 text-sky-800 border border-sky-200 px-1.5 py-0.5 rounded font-bold">{t.alimTuru}</span>
{t.sonucIlani && <span className="bg-emerald-50 text-emerald-800 border border-emerald-200 px-1.5 py-0.5 rounded font-bold">Sonuç İlanı</span>}
</div>
{(t.kazananFirma || t.yaklasikMaliyet != null) && (
<div className="flex items-center gap-4 flex-wrap pt-2 border-t border-slate-100 text-[11px]">
{t.kazananFirma && (
<span className="flex items-center gap-1 font-bold text-emerald-800">
<Trophy className="w-3 h-3" /> {t.kazananFirma}
{t.kazananUlke && <span className="text-slate-400 font-normal">({t.kazananUlke})</span>}
</span>
)}
{t.yaklasikMaliyet != null && (
<span className="font-black text-indigo-900">
{t.yaklasikMaliyet.toLocaleString('tr-TR')} {t.paraBirimi || ''}
</span>
)}
</div>
)}
</div>
))}
</div>
)}
<p className="text-[11px] text-slate-400 text-center">
Kaynak: Tenders Electronic Daily (ted.europa.eu) — Avrupa Birliği resmi ihale yayını.
</p>
</div>
);
};