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:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user