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
+222 -172
View File
@@ -1,71 +1,92 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useApp } from '../context/AppContext';
import { Sparkles, Upload, FileText, CheckCircle2, AlertTriangle, ShieldCheck, DollarSign, X, Loader2, ArrowRight, TrendingUp, Sliders } from 'lucide-react';
import { Sparkles, Upload, FileText, CheckCircle2, AlertTriangle, X, Loader2, TrendingUp, Sliders, Info } from 'lucide-react';
import confetti from 'canvas-confetti';
import { getAiConfig } from '../services/ai';
const tl = (n) => (typeof n === 'number' && isFinite(n) ? '₺' + n.toLocaleString('tr-TR') : '—');
export const AiSpecAnalyzerModal = () => {
const { isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen, analyzeSpecPdfWithGemini, aiAnalysisResults } = useApp();
const {
isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen,
analyzeSpecPdfWithGemini, aiAnalysisResults, activeAiModel, activeTenderDetail,
} = useApp();
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const [customBidSlider, setCustomBidSlider] = useState(148500);
const [error, setError] = useState(null);
const [progress, setProgress] = useState('');
const [customBid, setCustomBid] = useState(null);
const [aiCfg, setAiCfg] = useState(null);
useEffect(() => {
if (isAiSpecAnalyzerOpen && aiCfg === null) getAiConfig().then(setAiCfg);
}, [isAiSpecAnalyzerOpen, aiCfg]);
if (!isAiSpecAnalyzerOpen) return null;
const handleUploadAndAnalyze = async (fileName = 'Teknik_Sartname_Kayseri_Cezaevi.pdf') => {
const results = aiAnalysisResults;
const bid = results?.bidAnalysis || {};
const runAnalysis = async (text, fileName) => {
setError(null);
setIsAnalyzing(true);
setSelectedFile(fileName);
await analyzeSpecPdfWithGemini(fileName);
setIsAnalyzing(false);
confetti({ particleCount: 80, spread: 70 });
};
const results = aiAnalysisResults || {
fileName: 'Teknik_Sartname_Örnek.pdf',
model: 'Gemini 3.6 Flash',
analyzedAt: new Date().toLocaleString('tr-TR'),
summary: 'İşbu teknik şartname, idarenin ihtiyacı olan medikal sarf malzemeleri ve cihaz parçalarının teknik özellikleri, garanti şartları, teslimat vadeleri ve muayene kabul kriterlerini düzenlemektedir.',
qualificationCriteria: [
'TSE HYB Belgesi ve Yetkili Satıcı Satış Yetki Belgesi',
'ISO 13485 Medikal Cihaz Kalite Yönetim Sertifikası',
'ÜTS (Ürün Takip Sistemi) Barkod ve TİUBB Kaydı',
'En az %50 Oranında Benzer İş Deneyim Belgesi (Kamu/Özel)',
'Son 3 yıla ait Bilanço ve Gelir Tablosu oranları'
],
criticalRequirements: [
'Ürünler biyo-uyumluluk (ISO 10993) standartlarına uygun olmalıdır.',
'Teslim edilen paketlerin sterilizasyon son kullanma tarihi en az 18 ay olmalıdır.',
'Arıza durumunda 24 saat içinde yerinde teknik servis desteği sağlanmalıdır.',
'Teslimat aşamasında her parti için numune test raporu (CoA) ibraz edilmelidir.'
],
risksAndPenalties: [
{ level: 'Yüksek', clause: 'Madde 14.2', text: 'Gecikilen her takvim günü için ihale bedelinin %0.5 oranında cezai şart uygulanacaktır.' },
{ level: 'Orta', clause: 'Madde 18.1', text: 'Numune kontrolünde standart dışı bulunan partiler 3 iş günü içinde muadili ile ücretsiz değiştirilmelidir.' },
{ level: 'Düşük', clause: 'Madde 22.4', text: 'Ambalaj üzerindeki Türkçe etiket eksikliğinde kabul işlemi durdurulacaktır.' }
],
bidAnalysis: {
estimatedCost: 185000,
recommendedBid: 148500,
minBidLimit: 132000,
maxBidLimit: 165000,
winProbability: 89,
competitionLevel: 'Orta Yoğunlukta (3-4 Firma Katılımı Bekleniyor)',
historicalDiscountAvg: '%19.7 EKAP İndirim Oranı'
try {
setProgress('Model analiz ediyor…');
const r = await analyzeSpecPdfWithGemini({ text, fileName, tender: activeTenderDetail });
if (typeof r?.bidAnalysis?.recommendedBid === 'number') setCustomBid(r.bidAnalysis.recommendedBid);
confetti({ particleCount: 80, spread: 70 });
} catch (e) {
setError(e.message);
} finally {
setIsAnalyzing(false);
setProgress('');
}
};
// Win probability calculation based on custom bid slider
const calcProbability = (bid) => {
const rec = results.bidAnalysis.recommendedBid;
const diff = Math.abs(bid - rec);
const prob = Math.max(35, Math.min(96, Math.round(92 - (diff / 3000) * 3)));
return prob;
const handleFile = async (file) => {
setError(null);
setIsAnalyzing(true);
setSelectedFile(file.name);
try {
setProgress('Belge metni çıkarılıyor…');
const { extractText } = await import('../services/pdfText');
const { text, usedPages, pageCount } = await extractText(file);
if (!text || text.length < 40) {
throw new Error('Belgeden metin çıkarılamadı. Taranmış (görüntü) PDF olabilir — metin tabanlı bir dosya deneyin.');
}
setProgress(`${usedPages}/${pageCount} sayfa okundu, model analiz ediyor…`);
const r = await analyzeSpecPdfWithGemini({ text, fileName: file.name, tender: activeTenderDetail });
if (typeof r?.bidAnalysis?.recommendedBid === 'number') setCustomBid(r.bidAnalysis.recommendedBid);
confetti({ particleCount: 80, spread: 70 });
} catch (e) {
setError(e.message);
} finally {
setIsAnalyzing(false);
setProgress('');
}
};
// Açık ihalenin ilan metnini analiz et
const analyzeCurrentTender = () => {
const t = activeTenderDetail;
const text = [t?.baslik, t?.kurum, t?.aciklama, ...(t?.yeterlikKriterleri || [])].filter(Boolean).join('\n');
if (!text || text.length < 40) {
setError('Açık ihalenin ilan metni analiz için yeterli değil. Bir şartname dosyası yükleyin.');
return;
}
runAnalysis(text, `${t.ikn || 'ihale'} — ilan metni`);
};
const modelEtiketi = aiCfg?.providers?.length
? aiCfg.providers.map((p) => p.label).join(' → ')
: `${activeAiModel.label} (${activeAiModel.provider})`;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/75 backdrop-blur-xs animate-fade-in">
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xl w-full max-w-4xl overflow-hidden my-auto flex flex-col max-h-[92vh]">
{/* Header */}
<div className="bg-linear-to-r from-blue-900 via-indigo-900 to-purple-900 text-white px-6 py-4 flex items-center justify-between border-b border-indigo-800">
<div className="flex items-center gap-3">
@@ -73,191 +94,219 @@ export const AiSpecAnalyzerModal = () => {
<Sparkles className="w-6 h-6" />
</div>
<div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
<h2 className="font-extrabold text-base">Teknik Şartname & Teklif Analiz Asistanı</h2>
<span className="bg-amber-400 text-slate-900 text-[10px] font-black px-2 py-0.5 rounded-full uppercase tracking-wider">
Gemini 3.6 Flash Engine
{modelEtiketi}
</span>
</div>
<p className="text-xs text-blue-200">Teknik şartname PDF'inizi yükleyin; yeterlik, riskler ve önerilen teklif fiyat aralığını saniyeler içinde çıkaralım.</p>
<p className="text-xs text-blue-200">Şartname dosyanızı yükleyin; yeterlik, riskler ve teklif aralığı gerçek AI ile analiz edilsin.</p>
</div>
</div>
<button
onClick={() => setIsAiSpecAnalyzerOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg transition-colors"
>
<button onClick={() => setIsAiSpecAnalyzerOpen(false)} className="text-slate-400 hover:text-white p-1 rounded-lg transition-colors">
<X className="w-6 h-6" />
</button>
</div>
{/* Modal Content */}
<div className="p-6 space-y-6 overflow-y-auto">
{/* PDF Uploader Area */}
{/* AI yapılandırılmamış uyarısı */}
{aiCfg && !aiCfg.configured && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
<Info className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<p className="text-xs text-amber-900 leading-relaxed">
<strong>AI henüz yapılandırılmamış.</strong> Analizin çalışması için sunucuya en az bir sağlayıcı anahtarı ekleyin
(<code className="bg-amber-100 px-1 rounded">ZAI_API_KEY</code>, <code className="bg-amber-100 px-1 rounded">DEEPSEEK_API_KEY</code>,
{' '}<code className="bg-amber-100 px-1 rounded">GEMINI_API_KEY</code> …) ve yeniden dağıtın.
Model sırası, Admin → AI Model Ayarları'ndan belirlenir.
</p>
</div>
)}
{/* Uploader */}
<div className="bg-slate-50 border-2 border-dashed border-blue-300 hover:border-blue-500 rounded-2xl p-6 text-center space-y-3 transition-colors">
<div className="w-12 h-12 rounded-2xl bg-blue-100 text-blue-600 flex items-center justify-center mx-auto shadow-xs">
<Upload className="w-6 h-6" />
</div>
<div>
<h3 className="font-bold text-sm text-slate-800">Teknik Şartname PDF Dosyası Yükleyin</h3>
<p className="text-xs text-slate-500">Bilgisayarınızdan `.pdf` dosyanızı buraya sürükleyin veya örnek ihale şartnamesi seçin.</p>
<h3 className="font-bold text-sm text-slate-800">Teknik Şartname Dosyası Yükleyin</h3>
<p className="text-xs text-slate-500">Metin tabanlı <code>.pdf</code> veya <code>.txt</code> dosyası — belge tarayıcınızda okunur, yalnızca metni analiz için gönderilir.</p>
</div>
<div className="flex justify-center gap-3 pt-2">
<label className="bg-blue-600 hover:bg-blue-500 text-white font-extrabold text-xs px-5 py-2.5 rounded-xl cursor-pointer shadow-md transition-colors flex items-center gap-2">
<div className="flex justify-center gap-3 pt-2 flex-wrap">
<label className={`font-extrabold text-xs px-5 py-2.5 rounded-xl shadow-md transition-colors flex items-center gap-2 ${
isAnalyzing ? 'bg-slate-300 text-slate-500 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-500 text-white cursor-pointer'
}`}>
<Upload className="w-4 h-4" />
<span>PDF Dosyası Seç</span>
<span>Dosya Seç (PDF / TXT)</span>
<input
type="file"
accept=".pdf"
accept=".pdf,.txt,.md"
className="hidden"
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
handleUploadAndAnalyze(e.target.files[0].name);
}
}}
disabled={isAnalyzing}
onChange={(e) => { if (e.target.files?.[0]) handleFile(e.target.files[0]); }}
/>
</label>
<button
type="button"
onClick={() => handleUploadAndAnalyze('Örnek_EKAP_Kayseri_Cezaevi_Teknik_Sartname.pdf')}
className="bg-white hover:bg-slate-100 text-slate-700 font-bold text-xs px-4 py-2.5 rounded-xl border border-slate-300 transition-colors flex items-center gap-2"
>
<FileText className="w-4 h-4 text-purple-600" />
<span>Örnek EKAP Şartnamesi İle Dene</span>
</button>
{activeTenderDetail && (
<button
type="button"
disabled={isAnalyzing}
onClick={analyzeCurrentTender}
className="bg-white hover:bg-slate-100 disabled:opacity-50 text-slate-700 font-bold text-xs px-4 py-2.5 rounded-xl border border-slate-300 transition-colors flex items-center gap-2"
>
<FileText className="w-4 h-4 text-purple-600" />
<span>Açık İhalenin İlan Metnini Analiz Et</span>
</button>
)}
</div>
</div>
{/* Analyzing Loading Progress */}
{isAnalyzing && (
<div className="bg-linear-to-r from-blue-50 to-indigo-50 border border-blue-200 p-6 rounded-2xl space-y-3 animate-fade-in shadow-xs">
<div className="flex items-center justify-between font-extrabold text-xs text-blue-900">
<span className="flex items-center gap-2">
<Loader2 className="w-5 h-5 text-blue-600 animate-spin" />
⚡ Gemini 3.6 Flash PDF Multimodal Akışı İşleniyor...
</span>
<span className="bg-amber-400 text-slate-900 px-2 py-0.5 rounded font-black text-[10px]">Gemini 3.6 Flash</span>
</div>
<div className="space-y-1.5 text-xs text-slate-700 font-medium pt-2">
<p className="flex items-center gap-2">✓ 📄 {selectedFile} belgesindeki maddeler ayrıştırıldı.</p>
<p className="flex items-center gap-2">✓ 🛡️ Yeterlik sertifikaları ve cezai hükümler tespit ediliyor...</p>
<p className="flex items-center gap-2">✓ 📊 EKAP geçmiş ihale veri tabanından optimal teklif marjı hesaplanıyor...</p>
{/* Hata */}
{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">Analiz tamamlanamadı</p>
<p className="text-xs text-red-800 mt-0.5 leading-relaxed">{error}</p>
</div>
</div>
)}
{/* AI Specification Report Output */}
{/* Yükleniyor */}
{isAnalyzing && (
<div className="bg-linear-to-r from-blue-50 to-indigo-50 border border-blue-200 p-6 rounded-2xl space-y-2 animate-fade-in shadow-xs">
<div className="flex items-center gap-2 font-extrabold text-xs text-blue-900">
<Loader2 className="w-5 h-5 text-blue-600 animate-spin" />
<span>{progress || 'İşleniyor…'}</span>
</div>
<p className="text-xs text-slate-600">{selectedFile}</p>
</div>
)}
{/* Sonuç */}
{!isAnalyzing && results && (
<div className="space-y-6 animate-fade-in">
{/* Top Summary Banner */}
<div className="bg-slate-900 text-white p-5 rounded-2xl space-y-3 shadow-md">
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
<div className="flex items-center justify-between border-b border-slate-800 pb-2 gap-2 flex-wrap">
<span className="text-xs font-extrabold text-amber-400 uppercase tracking-wider flex items-center gap-1.5">
<Sparkles className="w-4 h-4" /> Gemini 3.6 Flash Şartname Özeti
<Sparkles className="w-4 h-4" /> Şartname Özeti
</span>
<span className="text-[10px] text-slate-400">Analiz: {results.analyzedAt}</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed font-medium">{results.summary}</p>
</div>
{/* Bidding Recommendation Box (Kritik Fark Yaratacak Özellik) */}
<div className="bg-linear-to-br from-emerald-950 via-slate-900 to-blue-950 text-white p-6 rounded-2xl space-y-4 shadow-xl border border-emerald-800/40">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-emerald-400" />
<h3 className="font-black text-base">Gemini 3.6 Flash Önerilen İhale Teklif Aralığı</h3>
</div>
<span className="bg-emerald-500 text-slate-950 font-black text-xs px-3 py-1 rounded-full shadow-xs">
%{calcProbability(customBidSlider)} Kazanma Şansı
<span className="text-[10px] text-slate-400">
{results.model} · {results.analyzedAt}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
<div className="bg-white/10 p-3.5 rounded-xl border border-white/10">
<span className="text-[11px] text-slate-300 font-medium block">İdare Yaklaşık Maliyeti</span>
<span className="text-base font-black text-white mt-1 block">
₺{results.bidAnalysis.estimatedCost.toLocaleString('tr-TR')}
</span>
</div>
<div className="bg-emerald-500/20 p-3.5 rounded-xl border border-emerald-400/40">
<span className="text-[11px] text-emerald-300 font-bold block">Önerilen Optimal Teklif</span>
<span className="text-xl font-black text-amber-300 mt-0.5 block">
₺{results.bidAnalysis.recommendedBid.toLocaleString('tr-TR')}
</span>
</div>
<div className="bg-white/10 p-3.5 rounded-xl border border-white/10">
<span className="text-[11px] text-slate-300 font-medium block">Geçmiş EKAP İndirim Ort.</span>
<span className="text-base font-black text-blue-300 mt-1 block">
{results.bidAnalysis.historicalDiscountAvg}
</span>
</div>
</div>
{/* Custom Bid Price Interactive Slider */}
<div className="bg-black/30 p-4 rounded-xl space-y-2 border border-white/10">
<div className="flex justify-between items-center text-xs">
<span className="font-bold text-slate-300 flex items-center gap-1.5">
<Sliders className="w-4 h-4 text-amber-400" /> Teklif Fiyatınızı Simüle Edin:
</span>
<span className="font-black text-sm text-emerald-400">₺{customBidSlider.toLocaleString('tr-TR')}</span>
</div>
<input
type="range"
min={results.bidAnalysis.minBidLimit}
max={results.bidAnalysis.maxBidLimit}
step={500}
value={customBidSlider}
onChange={(e) => setCustomBidSlider(Number(e.target.value))}
className="w-full accent-emerald-500 cursor-pointer h-2 bg-slate-800 rounded-lg"
/>
<div className="flex justify-between text-[10px] text-slate-400 font-medium">
<span>Min Limit: ₺{results.bidAnalysis.minBidLimit.toLocaleString('tr-TR')}</span>
<span className="text-amber-300 font-bold">Önerilen: ₺{results.bidAnalysis.recommendedBid.toLocaleString('tr-TR')}</span>
<span>Maks Limit: ₺{results.bidAnalysis.maxBidLimit.toLocaleString('tr-TR')}</span>
</div>
</div>
<p className="text-xs text-slate-300 leading-relaxed font-medium">{results.summary || 'Özet üretilemedi.'}</p>
{results.fileName && <p className="text-[10px] text-slate-500">Kaynak: {results.fileName}</p>}
</div>
{/* 2-Column Details: Qualifications & Risks */}
{/* Teklif analizi — yalnızca model sayı ürettiyse */}
{(bid.estimatedCost != null || bid.recommendedBid != null) && (
<div className="bg-linear-to-br from-emerald-950 via-slate-900 to-blue-950 text-white p-6 rounded-2xl space-y-4 shadow-xl border border-emerald-800/40">
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-emerald-400" />
<h3 className="font-black text-base">Önerilen Teklif Aralığı</h3>
</div>
{bid.winProbability != null && (
<span className="bg-emerald-500 text-slate-950 font-black text-xs px-3 py-1 rounded-full shadow-xs">
%{bid.winProbability} Kazanma Tahmini
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
<div className="bg-white/10 p-3.5 rounded-xl border border-white/10">
<span className="text-[11px] text-slate-300 font-medium block">İdare Yaklaşık Maliyeti</span>
<span className="text-base font-black text-white mt-1 block">{tl(bid.estimatedCost)}</span>
</div>
<div className="bg-emerald-500/20 p-3.5 rounded-xl border border-emerald-400/40">
<span className="text-[11px] text-emerald-300 font-bold block">Önerilen Optimal Teklif</span>
<span className="text-xl font-black text-amber-300 mt-0.5 block">{tl(bid.recommendedBid)}</span>
</div>
<div className="bg-white/10 p-3.5 rounded-xl border border-white/10">
<span className="text-[11px] text-slate-300 font-medium block">Rekabet Değerlendirmesi</span>
<span className="text-xs font-bold text-blue-300 mt-1 block leading-snug">{bid.competitionLevel || '—'}</span>
</div>
</div>
{bid.minBidLimit != null && bid.maxBidLimit != null && bid.maxBidLimit > bid.minBidLimit && (
<div className="bg-black/30 p-4 rounded-xl space-y-2 border border-white/10">
<div className="flex justify-between items-center text-xs">
<span className="font-bold text-slate-300 flex items-center gap-1.5">
<Sliders className="w-4 h-4 text-amber-400" /> Teklif Fiyatınızı Simüle Edin:
</span>
<span className="font-black text-sm text-emerald-400">{tl(customBid ?? bid.recommendedBid)}</span>
</div>
<input
type="range"
min={bid.minBidLimit}
max={bid.maxBidLimit}
step={Math.max(1, Math.round((bid.maxBidLimit - bid.minBidLimit) / 200))}
value={customBid ?? bid.recommendedBid ?? bid.minBidLimit}
onChange={(e) => setCustomBid(Number(e.target.value))}
className="w-full accent-emerald-500 cursor-pointer h-2 bg-slate-800 rounded-lg"
/>
<div className="flex justify-between text-[10px] text-slate-400 font-medium">
<span>Min: {tl(bid.minBidLimit)}</span>
<span className="text-amber-300 font-bold">Önerilen: {tl(bid.recommendedBid)}</span>
<span>Maks: {tl(bid.maxBidLimit)}</span>
</div>
</div>
)}
{bid.notes && <p className="text-[11px] text-slate-400 leading-relaxed">{bid.notes}</p>}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Qualification Criteria */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<h4 className="font-extrabold text-sm text-slate-900 flex items-center gap-2 border-b border-slate-100 pb-2">
<CheckCircle2 className="w-4 h-4 text-emerald-600" /> Yeterlik Kriterleri & Zorunlu Belgeler
</h4>
<ul className="space-y-2 text-xs text-slate-700">
{results.qualificationCriteria.map((item, idx) => (
{(results.qualificationCriteria || []).length === 0 && (
<li className="text-slate-400">Belgede yeterlik kriteri tespit edilemedi.</li>
)}
{(results.qualificationCriteria || []).map((item, idx) => (
<li key={idx} className="flex items-start gap-2 bg-slate-50 p-2.5 rounded-lg border border-slate-200 font-medium">
<span className="w-2 h-2 rounded-full bg-emerald-600 mt-1.5 shrink-0"></span>
<span>{item}</span>
</li>
))}
</ul>
{(results.criticalRequirements || []).length > 0 && (
<>
<h4 className="font-extrabold text-xs text-slate-900 pt-2 border-t border-slate-100">Kritik Şartlar</h4>
<ul className="space-y-1.5 text-xs text-slate-700">
{results.criticalRequirements.map((item, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-600 mt-1.5 shrink-0"></span>
<span>{item}</span>
</li>
))}
</ul>
</>
)}
</div>
{/* Risks & Penalty Clauses */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<h4 className="font-extrabold text-sm text-slate-900 flex items-center gap-2 border-b border-slate-100 pb-2">
<AlertTriangle className="w-4 h-4 text-rose-600" /> Riskler & Şartname Cezai Hükümleri
<AlertTriangle className="w-4 h-4 text-rose-600" /> Riskler & Cezai Hükümler
</h4>
<div className="space-y-2 text-xs">
{results.risksAndPenalties.map((risk, idx) => (
{(results.risksAndPenalties || []).length === 0 && (
<p className="text-slate-400">Belgede cezai hüküm/risk tespit edilemedi.</p>
)}
{(results.risksAndPenalties || []).map((risk, idx) => (
<div key={idx} className="p-3 bg-slate-50 rounded-lg border border-slate-200 space-y-1">
<div className="flex items-center justify-between">
<span className="font-bold text-slate-900">{risk.clause}</span>
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded ${
<div className="flex items-center justify-between gap-2">
<span className="font-bold text-slate-900">{risk.clause || '—'}</span>
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded shrink-0 ${
risk.level === 'Yüksek' ? 'bg-red-100 text-red-800' :
risk.level === 'Orta' ? 'bg-amber-100 text-amber-800' : 'bg-slate-200 text-slate-800'
}`}>
{risk.level} Risk
{risk.level || 'Bilgi'} Risk
</span>
</div>
<p className="text-slate-600 leading-snug">{risk.text}</p>
@@ -265,15 +314,16 @@ export const AiSpecAnalyzerModal = () => {
))}
</div>
</div>
</div>
<p className="text-[10px] text-slate-400 text-center leading-relaxed">
Analiz yapay zeka tarafından üretilmiştir; teklif kararı öncesi şartnamenin aslını doğrulayın.
</p>
</div>
)}
</div>
{/* Footer */}
<div className="bg-slate-50 p-4 border-t border-slate-200 flex justify-end">
<button
onClick={() => setIsAiSpecAnalyzerOpen(false)}