Files
ihaleist/src/components/tools/RedFlags.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

120 lines
6.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 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>
);
};