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:
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { redFlags, marketConcentration, formatTL } from '../../services/analytics';
|
||||
import { ShieldAlert, AlertTriangle, TrendingDown, Building2, Users, Info } from 'lucide-react';
|
||||
|
||||
const SEVIYE = {
|
||||
yuksek: { etiket: 'Yüksek', kutu: 'bg-red-50 border-red-200', rozet: 'bg-red-100 text-red-800', cizgi: 'bg-red-500' },
|
||||
orta: { etiket: 'Orta', kutu: 'bg-amber-50 border-amber-200', rozet: 'bg-amber-100 text-amber-800', cizgi: 'bg-amber-500' },
|
||||
dusuk: { etiket: 'Düşük', kutu: 'bg-slate-50 border-slate-200', rozet: 'bg-slate-100 text-slate-700', cizgi: 'bg-slate-400' },
|
||||
};
|
||||
|
||||
const TUR_IKON = {
|
||||
'Tek teklif': Users,
|
||||
'Düşük kırım': TrendingDown,
|
||||
'Tahmin üstü bedel': AlertTriangle,
|
||||
'Aşırı düşük teklif': TrendingDown,
|
||||
'Tedarikçi yoğunlaşması': Building2,
|
||||
};
|
||||
|
||||
export const RedFlags = () => {
|
||||
const { tenders, setActiveTenderDetail } = useApp();
|
||||
const { flags, yuksek, orta, analiz } = redFlags(tenders);
|
||||
const { hhi, seviye } = marketConcentration(tenders);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-linear-to-r from-slate-900 via-rose-950 to-red-900 text-white p-6 rounded-2xl shadow-lg border border-rose-800/40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-2xl bg-rose-600 text-white flex items-center justify-center shadow-md">
|
||||
<ShieldAlert className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-extrabold text-xl">Kırmızı Bayrak & Şeffaflık Analizi</h2>
|
||||
<p className="text-xs text-rose-200 mt-0.5">Tek teklifli ihaleler, olağandışı kırım oranları ve tedarikçi yoğunlaşması otomatik tespit edilir.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Özet kartlar */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Yüksek Öncelik</span>
|
||||
<span className="text-2xl font-black text-red-600 mt-1 block">{yuksek}</span>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Orta Öncelik</span>
|
||||
<span className="text-2xl font-black text-amber-600 mt-1 block">{orta}</span>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Analiz Edilen</span>
|
||||
<span className="text-2xl font-black text-slate-900 mt-1 block">{analiz}</span>
|
||||
<span className="text-[11px] text-slate-400">sonuç verisi olan ihale</span>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Pazar Yoğunlaşması</span>
|
||||
<span className="text-2xl font-black text-indigo-900 mt-1 block">{hhi != null ? hhi : '—'}</span>
|
||||
<span className="text-[11px] text-slate-400">{seviye || 'HHI için sonuç verisi gerekli'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yöntem notu */}
|
||||
<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">
|
||||
Bu sinyaller kamuya açık ihale verisinden otomatik hesaplanır ve <strong>incelenmeye değer durumları</strong> işaret eder —
|
||||
tek başlarına usulsüzlük kanıtı değildir. Tek teklif ve kırım analizleri, ilgili alanların (teklif sayısı, yaklaşık maliyet,
|
||||
sözleşme bedeli) veride bulunduğu ihaleler için üretilir.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bayrak listesi */}
|
||||
{flags.length === 0 ? (
|
||||
<div className="bg-white rounded-2xl border border-dashed border-slate-300 p-10 text-center">
|
||||
<ShieldAlert className="w-8 h-8 text-slate-300 mx-auto mb-3" />
|
||||
<p className="text-sm font-bold text-slate-500">Henüz kırmızı bayrak tespit edilmedi</p>
|
||||
<p className="text-xs text-slate-400 mt-1 max-w-md mx-auto leading-relaxed">
|
||||
Analiz için sonuç verisi (kazanan firma, sözleşme bedeli, teklif sayısı) gerekir.
|
||||
Doğrudan temin sonuçları ve EKAP sonuç ilanları biriktikçe bu liste otomatik dolar.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{flags.map((f) => {
|
||||
const s = SEVIYE[f.seviye];
|
||||
const Icon = TUR_IKON[f.tur] || AlertTriangle;
|
||||
return (
|
||||
<div key={f.id} className={`rounded-xl border ${s.kutu} p-4 flex gap-3 items-start`}>
|
||||
<span className={`w-1 self-stretch rounded-full ${s.cizgi} shrink-0`} />
|
||||
<Icon className="w-4 h-4 text-slate-600 shrink-0 mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded-full ${s.rozet}`}>{s.etiket}</span>
|
||||
<span className="text-xs font-extrabold text-slate-900">{f.tur}</span>
|
||||
{f.deger && <span className="text-[11px] font-bold text-slate-600 bg-white/70 border border-slate-200 px-2 py-0.5 rounded">{f.deger}</span>}
|
||||
</div>
|
||||
<p className="text-xs text-slate-700 mt-1.5 leading-relaxed">{f.aciklama}</p>
|
||||
{f.tender ? (
|
||||
<button
|
||||
onClick={() => setActiveTenderDetail(f.tender)}
|
||||
className="text-[11px] font-bold text-blue-700 hover:underline mt-1.5 text-left"
|
||||
>
|
||||
{f.tender.baslik}
|
||||
{f.tender.kurum ? ` — ${f.tender.kurum}` : ''}
|
||||
{f.tender.sozlesmeBedeli != null ? ` (${formatTL(Number(f.tender.sozlesmeBedeli))})` : ''}
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-[11px] font-bold text-slate-600 mt-1.5">{f.kurum}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user