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)}
+312
View File
@@ -0,0 +1,312 @@
import React, { useEffect, useRef, useState } from 'react';
import {
ArrowRight, Bell, Building2, CalendarDays, ChartNoAxesCombined, CheckCircle2,
FileSearch, Gavel, Kanban, MapPin, Radar, Sparkles, Timer, TrendingUp, Zap,
} from 'lucide-react';
/* ---------- yardımcılar ---------- */
const useReveal = () => {
useEffect(() => {
const els = document.querySelectorAll('.lp-reveal');
const io = new IntersectionObserver(
(entries) => entries.forEach((e) => e.isIntersecting && e.target.classList.add('lp-in')),
{ threshold: 0.14 }
);
els.forEach((el) => io.observe(el));
return () => io.disconnect();
}, []);
};
const CountUp = ({ to, suffix = '', duration = 1600 }) => {
const ref = useRef(null);
const [val, setVal] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
let raf, started = false;
const io = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting || started) return;
started = true;
const t0 = performance.now();
const tick = (t) => {
const p = Math.min(1, (t - t0) / duration);
setVal(Math.round(to * (1 - Math.pow(1 - p, 3))));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
}, { threshold: 0.5 });
io.observe(el);
return () => { io.disconnect(); cancelAnimationFrame(raf); };
}, [to, duration]);
return <span ref={ref}>{val.toLocaleString('tr-TR')}{suffix}</span>;
};
/* ---------- örnek akış verisi (görsel amaçlı) ---------- */
const TICKER = [
{ k: 'Yapım İşi', t: 'Okul binası güçlendirme', c: 'Ankara', renk: '#fbbf24' },
{ k: 'Mal Alımı', t: 'Tıbbi sarf malzemesi', c: 'İzmir', renk: '#22d3ee' },
{ k: 'Hizmet', t: 'Personel taşıma 12 ay', c: 'Bursa', renk: '#a78bfa' },
{ k: '22/d', t: 'Bilgisayar ve çevre birimleri', c: 'İstanbul', renk: '#34d399' },
{ k: 'Yapım İşi', t: 'İçme suyu hattı yenileme', c: 'Konya', renk: '#fbbf24' },
{ k: 'Mal Alımı', t: 'Araç lastiği alımı', c: 'Antalya', renk: '#22d3ee' },
{ k: 'Hizmet', t: 'Yemek hazırlama ve dağıtım', c: 'Gaziantep', renk: '#a78bfa' },
{ k: '22/d', t: 'Kırtasiye malzemesi', c: 'Trabzon', renk: '#34d399' },
];
const OZELLIKLER = [
{ icon: Zap, baslik: 'Canlı ihale akışı', metin: 'ilan.gov.tr ilanları dakikalar içinde ekranınızda. Şehir, sektör ve bütçeye göre süzün; hiçbir fırsatı kaçırmayın.', renk: 'from-cyan-400/20 to-cyan-400/0', ikon: 'text-cyan-300' },
{ icon: FileSearch, baslik: 'AI şartname analizi', metin: 'Yüzlerce sayfalık şartnameyi yükleyin; yeterlilik şartları, riskler ve kritik tarihler saniyeler içinde özetlensin.', renk: 'from-amber-400/20 to-amber-400/0', ikon: 'text-amber-300' },
{ icon: Kanban, baslik: 'Kanban süreç takibi', metin: 'İlgilendiğiniz ihaleleri panoya taşıyın: İnceleme → Teklif Hazırlığı → Verildi → Sonuç. Ekipçe aynı ekranda.', renk: 'from-violet-400/20 to-violet-400/0', ikon: 'text-violet-300' },
{ icon: Radar, baslik: 'Rakip radarı', metin: 'Hangi firma hangi ihaleyi, hangi bedelle kazandı? Rakiplerinizin izini sürün, stratejinizi veriyle kurun.', renk: 'from-rose-400/20 to-rose-400/0', ikon: 'text-rose-300' },
{ icon: TrendingUp, baslik: 'Fiyat & piyasa analizi', metin: 'Geçmiş sonuçlardan yaklaşık maliyet ve kırım oranı analizi. Teklifinizi tahminle değil, veriyle verin.', renk: 'from-emerald-400/20 to-emerald-400/0', ikon: 'text-emerald-300' },
{ icon: Bell, baslik: 'Günlük bülten & uyarılar', metin: 'Kriterlerinize uyan yeni ilanlar her sabah e-postanızda. Son teklif tarihi yaklaşınca otomatik hatırlatma.', renk: 'from-sky-400/20 to-sky-400/0', ikon: 'text-sky-300' },
];
/* ---------- bileşenler ---------- */
const TickerRow = ({ reverse = false }) => (
<div className="lp-ticker-mask overflow-hidden">
<div className={`flex w-max gap-3 ${reverse ? 'lp-marquee-rev' : 'lp-marquee'}`}>
{[...TICKER, ...TICKER].map((x, i) => (
<div key={i} className="flex shrink-0 items-center gap-2.5 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm backdrop-blur">
<span className="h-2 w-2 rounded-full" style={{ background: x.renk }} />
<span className="font-semibold text-white/90">{x.t}</span>
<span className="text-white/40">·</span>
<span className="flex items-center gap-1 text-white/50"><MapPin size={12} />{x.c}</span>
<span className="rounded-full px-2 py-0.5 text-[11px] font-bold" style={{ background: `${x.renk}22`, color: x.renk }}>{x.k}</span>
</div>
))}
</div>
</div>
);
const MockDashboard = () => (
<div className="relative mx-auto w-full max-w-xl lp-float">
<div className="absolute -inset-8 rounded-[2.5rem] bg-gradient-to-tr from-cyan-500/25 via-violet-500/15 to-amber-400/25 blur-3xl" aria-hidden="true" />
<div className="relative overflow-hidden rounded-2xl border border-white/12 bg-[#101a2e] shadow-[0_40px_120px_-30px_rgba(34,211,238,0.35)]">
<div className="flex h-11 items-center gap-2 border-b border-white/8 bg-white/[0.04] px-4">
<span className="h-2.5 w-2.5 rounded-full bg-rose-400/80" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-400/80" />
<span className="h-2.5 w-2.5 rounded-full bg-emerald-400/80" />
<span className="ml-3 flex items-center gap-1.5 text-xs font-semibold text-white/50">
<span className="lp-blink h-1.5 w-1.5 rounded-full bg-emerald-400" /> ihaleist · canlı akış
</span>
</div>
<div className="space-y-2.5 p-4">
{[
{ t: 'Hastane ek bina inşaatı', b: '₺48,2M', s: 'Teklif Hazırlığı', sr: 'bg-amber-400/15 text-amber-300', g: 8, d: 0 },
{ t: 'Ağ altyapısı yenileme', b: '₺6,7M', s: 'Yeni İlan', sr: 'bg-cyan-400/15 text-cyan-300', g: 3, d: 1 },
{ t: 'Araç kiralama 24 ay', b: '₺12,4M', s: 'İnceleme', sr: 'bg-violet-400/15 text-violet-300', g: 15, d: 2 },
].map((r, i) => (
<div key={i} className="lp-card-in rounded-xl border border-white/8 bg-white/[0.04] p-3.5 transition-colors hover:bg-white/[0.07]" style={{ animationDelay: `${0.35 + r.d * 0.22}s` }}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<Gavel size={14} className="shrink-0 text-cyan-300" />
<span className="truncate text-sm font-bold text-white">{r.t}</span>
</div>
<div className="mt-1.5 flex items-center gap-3 text-xs text-white/45">
<span className="flex items-center gap-1"><Building2 size={11} /> Kamu kurumu</span>
<span className="flex items-center gap-1"><Timer size={11} /> {r.g} gün kaldı</span>
</div>
</div>
<div className="text-right">
<div className="font-mono text-sm font-black text-emerald-300">{r.b}</div>
<span className={`mt-1 inline-block rounded-full px-2 py-0.5 text-[10px] font-bold ${r.sr}`}>{r.s}</span>
</div>
</div>
</div>
))}
<div className="grid grid-cols-4 items-end gap-2 rounded-xl border border-white/8 bg-white/[0.04] p-3.5">
<div className="col-span-4 mb-1 flex items-center justify-between">
<span className="flex items-center gap-1.5 text-xs font-semibold text-white/60"><ChartNoAxesCombined size={13} className="text-amber-300" /> Kırım oranı analizi</span>
<span className="text-[10px] font-bold text-white/35">SON 6 AY</span>
</div>
{[42, 68, 55, 80, 62, 90, 74, 96].map((h, i) => (
<div key={i} className="lp-bar rounded-t-md bg-gradient-to-t from-cyan-500/60 to-cyan-300" style={{ '--h': `${h * 0.5}px`, animationDelay: `${0.9 + i * 0.09}s` }} />
))}
</div>
</div>
</div>
<div className="lp-sticker absolute -right-4 -top-5 rotate-6 rounded-xl border border-amber-300/30 bg-amber-400 px-3.5 py-2 text-xs font-black text-amber-950 shadow-lg shadow-amber-500/30">
<Sparkles size={13} className="mr-1 inline" /> AI özetledi: 3 kritik şart
</div>
<div className="lp-sticker absolute -bottom-4 -left-3 -rotate-3 rounded-xl border border-white/15 bg-[#16233c] px-3.5 py-2 text-xs font-bold text-white shadow-xl" style={{ animationDelay: '1.4s' }}>
🔔 Bugün size uyan <span className="text-cyan-300">14 yeni ilan</span>
</div>
</div>
);
const Adim = ({ no, baslik, metin, gecikme }) => (
<div className="lp-reveal group relative rounded-2xl border border-white/10 bg-white/[0.04] p-7 transition duration-300 hover:-translate-y-1.5 hover:border-cyan-300/30 hover:bg-white/[0.06]" style={{ transitionDelay: gecikme }}>
<div className="font-mono text-5xl font-black text-white/8 transition group-hover:text-cyan-300/20">{no}</div>
<h3 className="mt-4 text-xl font-black text-white">{baslik}</h3>
<p className="mt-2.5 text-sm leading-7 text-white/55">{metin}</p>
</div>
);
/* ---------- sayfa ---------- */
export const LandingPage = ({ onGiris }) => {
useReveal();
const yil = new Date().getFullYear();
return (
<div className="lp-root min-h-screen overflow-x-hidden bg-[#0a1122] text-white antialiased">
{/* arkaplan */}
<div className="pointer-events-none fixed inset-0" aria-hidden="true">
<div className="lp-blob absolute -top-40 left-1/4 h-[520px] w-[520px] rounded-full bg-cyan-500/14 blur-[110px]" />
<div className="lp-blob absolute right-[-10%] top-1/3 h-[460px] w-[460px] rounded-full bg-violet-500/12 blur-[110px]" style={{ animationDelay: '-6s' }} />
<div className="lp-blob absolute bottom-[-15%] left-[-5%] h-[420px] w-[420px] rounded-full bg-amber-400/10 blur-[110px]" style={{ animationDelay: '-12s' }} />
<div className="lp-grid absolute inset-0 opacity-[0.35]" />
</div>
{/* nav */}
<header className="relative z-40 mx-auto flex max-w-7xl items-center justify-between px-5 py-5 sm:px-8">
<div className="flex items-center gap-2.5">
<span className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-400 to-blue-600 shadow-lg shadow-cyan-500/30">
<Gavel size={20} className="text-white" />
</span>
<span className="text-xl font-black tracking-tight">ihale<span className="text-cyan-300">ist</span></span>
</div>
<nav className="hidden items-center gap-8 text-sm font-semibold text-white/60 md:flex">
<a href="#ozellikler" className="transition hover:text-white">Özellikler</a>
<a href="#nasil" className="transition hover:text-white">Nasıl çalışır?</a>
<a href="#rakamlar" className="transition hover:text-white">Rakamlarla</a>
</nav>
<button onClick={onGiris} className="group flex items-center gap-2 rounded-xl bg-white px-5 py-2.5 text-sm font-black text-slate-900 transition hover:bg-cyan-300">
Giriş Yap <ArrowRight size={15} className="transition group-hover:translate-x-0.5" />
</button>
</header>
{/* hero */}
<section className="relative z-10 mx-auto max-w-7xl px-5 pb-16 pt-10 sm:px-8 sm:pt-16">
<div className="grid items-center gap-14 lg:grid-cols-[1.02fr_0.98fr]">
<div>
<div className="lp-hero-in inline-flex items-center gap-2 rounded-full border border-cyan-300/25 bg-cyan-400/10 px-4 py-1.5 text-xs font-bold text-cyan-200">
<span className="lp-blink h-1.5 w-1.5 rounded-full bg-emerald-400" />
ilan.gov.tr akışı şu anda canlı
</div>
<h1 className="lp-hero-in mt-6 text-5xl font-black leading-[1.04] tracking-tight sm:text-6xl xl:text-7xl" style={{ animationDelay: '0.08s' }}>
Kamu ihalelerini
<br />
<span className="lp-gradient-text">kaçırmayan</span> firma,
<br />
kazanan firmadır.
</h1>
<p className="lp-hero-in mt-6 max-w-xl text-lg leading-8 text-white/60" style={{ animationDelay: '0.16s' }}>
İhaleist; canlı ilan akışını, yapay zekâ destekli şartname analizini, rakip takibini ve
fiyat analizini tek panelde toplar. Siz teklife odaklanın, takibi biz yapalım.
</p>
<div className="lp-hero-in mt-9 flex flex-col gap-3 sm:flex-row" style={{ animationDelay: '0.24s' }}>
<button onClick={onGiris} className="group flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-cyan-400 to-blue-500 px-7 py-3.5 text-base font-black text-white shadow-xl shadow-cyan-500/25 transition hover:shadow-cyan-400/40 hover:brightness-110">
Hemen Başla — Ücretsiz <ArrowRight size={17} className="transition group-hover:translate-x-1" />
</button>
<a href="#ozellikler" className="flex items-center justify-center gap-2 rounded-xl border border-white/15 bg-white/[0.05] px-7 py-3.5 text-base font-bold text-white/85 backdrop-blur transition hover:border-white/30 hover:bg-white/10">
Neler yapabilir?
</a>
</div>
<div className="lp-hero-in mt-7 flex flex-wrap gap-x-6 gap-y-2 text-sm font-medium text-white/45" style={{ animationDelay: '0.32s' }}>
<span className="flex items-center gap-1.5"><CheckCircle2 size={15} className="text-emerald-400" /> Google ile 10 saniyede giriş</span>
<span className="flex items-center gap-1.5"><CheckCircle2 size={15} className="text-emerald-400" /> Kurulum gerekmez</span>
<span className="flex items-center gap-1.5"><CheckCircle2 size={15} className="text-emerald-400" /> KVKK uyumlu</span>
</div>
</div>
<MockDashboard />
</div>
</section>
{/* akış şeridi */}
<section className="relative z-10 space-y-3 py-6">
<TickerRow />
<TickerRow reverse />
</section>
{/* rakamlar */}
<section id="rakamlar" className="relative z-10 mx-auto max-w-7xl scroll-mt-24 px-5 py-16 sm:px-8">
<div className="lp-reveal grid gap-4 rounded-3xl border border-white/10 bg-gradient-to-r from-cyan-500/[0.08] via-white/[0.04] to-violet-500/[0.08] p-8 sm:grid-cols-2 sm:p-10 lg:grid-cols-4">
{[
{ v: 2400, s: '+', l: 'Günlük yeni ilan taranıyor', i: Zap, r: 'text-cyan-300' },
{ v: 81, s: '', l: 'İl genelinde takip', i: MapPin, r: 'text-amber-300' },
{ v: 15000, s: '+', l: 'Kayıtlı kamu kurumu', i: Building2, r: 'text-violet-300' },
{ v: 30, s: ' sn', l: 'Ortalama şartname özeti', i: Sparkles, r: 'text-emerald-300' },
].map((x) => (
<div key={x.l} className="flex flex-col items-center gap-1 text-center">
<x.i size={22} className={x.r} />
<div className="mt-2 text-4xl font-black tracking-tight"><CountUp to={x.v} suffix={x.s} /></div>
<div className="text-sm font-medium text-white/50">{x.l}</div>
</div>
))}
</div>
</section>
{/* özellikler */}
<section id="ozellikler" className="relative z-10 mx-auto max-w-7xl scroll-mt-24 px-5 py-16 sm:px-8">
<div className="lp-reveal max-w-2xl">
<span className="text-xs font-black uppercase tracking-[0.22em] text-cyan-300">Tek panel, tam kontrol</span>
<h2 className="mt-4 text-4xl font-black tracking-tight sm:text-5xl">İhale masanızın <span className="lp-underline">tamamı</span> burada.</h2>
<p className="mt-4 text-lg leading-8 text-white/55">EKAP'ta, gazete ilanlarında, kurum sitelerinde dağınık duran her şey — tek akışta, süzülmüş ve önceliklendirilmiş.</p>
</div>
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{OZELLIKLER.map((o, i) => (
<article key={o.baslik} className="lp-reveal group relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04] p-7 transition duration-300 hover:-translate-y-1.5 hover:border-white/25" style={{ transitionDelay: `${(i % 3) * 90}ms` }}>
<div className={`pointer-events-none absolute inset-0 bg-gradient-to-br opacity-0 transition duration-300 group-hover:opacity-100 ${o.renk}`} aria-hidden="true" />
<div className="relative">
<span className={`flex h-12 w-12 items-center justify-center rounded-xl border border-white/10 bg-white/[0.06] ${o.ikon}`}><o.icon size={22} /></span>
<h3 className="mt-6 text-xl font-black">{o.baslik}</h3>
<p className="mt-2.5 text-sm leading-7 text-white/55">{o.metin}</p>
</div>
</article>
))}
</div>
</section>
{/* nasıl çalışır */}
<section id="nasil" className="relative z-10 mx-auto max-w-7xl scroll-mt-24 px-5 py-16 sm:px-8">
<div className="lp-reveal text-center">
<span className="text-xs font-black uppercase tracking-[0.22em] text-amber-300">Nasıl çalışır?</span>
<h2 className="mt-4 text-4xl font-black tracking-tight sm:text-5xl">Üç adımda ihale radarınız hazır.</h2>
</div>
<div className="mt-12 grid gap-5 lg:grid-cols-3">
<Adim no="01" baslik="Giriş yapın, kriterinizi kurun" metin="Google hesabınızla girin; sektör, şehir ve bütçe aralığınızı seçin. Akıllı filtre motoru gerisini halleder." gecikme="0ms" />
<Adim no="02" baslik="Akışı ve bülteni takip edin" metin="Size uyan ilanlar panelde ve her sabah e-posta bülteninizde. Şartnameyi AI'a özetletin, riskleri görün." gecikme="120ms" />
<Adim no="03" baslik="Teklif verin, süreci yönetin" metin="Kanban'da süreci ekiple yürütün; sonuç ilanlarından rakip ve fiyat analizine geçip bir sonraki teklifinizi güçlendirin." gecikme="240ms" />
</div>
</section>
{/* son çağrı */}
<section className="relative z-10 mx-auto max-w-7xl px-5 py-20 sm:px-8">
<div className="lp-reveal relative overflow-hidden rounded-[2rem] border border-cyan-300/20 bg-gradient-to-br from-[#0e2036] via-[#101a2e] to-[#1a1035] px-7 py-16 text-center shadow-2xl sm:px-14">
<div className="lp-blob absolute -right-24 -top-24 h-72 w-72 rounded-full bg-cyan-400/15 blur-[90px]" aria-hidden="true" />
<div className="lp-blob absolute -bottom-24 -left-24 h-72 w-72 rounded-full bg-amber-400/12 blur-[90px]" style={{ animationDelay: '-8s' }} aria-hidden="true" />
<div className="relative">
<CalendarDays size={34} className="mx-auto text-cyan-300" />
<h2 className="mx-auto mt-5 max-w-2xl text-4xl font-black tracking-tight sm:text-5xl">Yarınki ilanlar sizi beklemeyecek.</h2>
<p className="mx-auto mt-4 max-w-xl text-lg leading-8 text-white/55">Bugün girin, kriterlerinizi kurun; yarın sabah ilk bülteniniz hazır olsun.</p>
<button onClick={onGiris} className="group mx-auto mt-9 flex items-center gap-2 rounded-xl bg-gradient-to-r from-cyan-400 to-blue-500 px-8 py-4 text-lg font-black text-white shadow-xl shadow-cyan-500/30 transition hover:shadow-cyan-400/50 hover:brightness-110">
Ücretsiz Giriş Yap <ArrowRight size={19} className="transition group-hover:translate-x-1" />
</button>
</div>
</div>
</section>
{/* footer */}
<footer className="relative z-10 border-t border-white/8 px-5 py-9 sm:px-8">
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-5 text-sm text-white/40 sm:flex-row">
<div className="flex items-center gap-2">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-cyan-400 to-blue-600"><Gavel size={14} className="text-white" /></span>
<span className="font-black text-white/80">ihale<span className="text-cyan-300">ist</span></span>
<span className="ml-2">© {yil}</span>
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 font-semibold">
<a href="/gizlilik.html" className="transition hover:text-white">Gizlilik Politikası</a>
<a href="/kullanim-kosullari.html" className="transition hover:text-white">Kullanım Şartları</a>
<button onClick={onGiris} className="transition hover:text-white">Giriş</button>
</div>
</div>
</footer>
</div>
);
};
+5 -1
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { useApp } from '../context/AppContext';
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone, Cpu } from 'lucide-react';
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone, Cpu, ShieldAlert, Globe2, Gavel, CreditCard } from 'lucide-react';
export const TopNavigation = () => {
const { activeTab, setActiveTab, isAdmin } = useApp();
@@ -17,6 +17,10 @@ export const TopNavigation = () => {
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar, badge: 'Canlı Uyar' },
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
{ id: 'kirmizi_bayrak', label: 'Kırmızı Bayrak', icon: ShieldAlert, badge: 'Şeffaflık' },
{ id: 'kik_kararlari', label: 'KİK Kurul Kararları', icon: Gavel },
{ id: 'ted_eu', label: 'AB İhaleleri (TED)', icon: Globe2 },
{ id: 'planlar', label: 'Planlar', icon: CreditCard },
{ id: 'api_webhooks', label: 'API & Webhooks', icon: Webhook },
// Sadece admin: AI Model Ayarları
...(isAdmin ? [{ id: 'ai_ayarlari', label: 'AI Model Ayarları', icon: Cpu, badge: 'Admin' }] : []),
+165
View File
@@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Gavel, Search, Loader2, ExternalLink, AlertTriangle, Calendar, Building2, Info } from 'lucide-react';
export const KikDecisions = () => {
const { tenders } = useApp();
const [q, setQ] = useState('');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const ara = async (terim) => {
const t = (terim ?? q).trim();
if (!t) return;
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/kik/search?q=${encodeURIComponent(t)}&limit=20`);
const d = await res.json();
if (d.success) setData(d);
else { setError(d.error || 'Arama başarısız'); setData(null); }
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
// Takipteki ihalelerin idarelerinden hızlı arama önerileri
const oneriler = [...new Set(
tenders.filter((t) => t.takipte || t.kanbanStatus).map((t) => t.kurum).filter(Boolean)
)].slice(0, 4);
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-linear-to-r from-slate-900 via-amber-950 to-orange-900 text-white p-6 rounded-2xl shadow-lg border border-amber-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-amber-600 flex items-center justify-center shadow-md">
<Gavel className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">KİK Kurul Kararları</h2>
<p className="text-xs text-amber-200 mt-0.5">
Kamu İhale Kurulu itirazen şikâyet kararlarında arama yapın; emsal kararları teklif öncesi inceleyin.
</p>
</div>
</div>
</div>
{/* Arama */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
value={q}
onChange={(e) => setQ(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && ara()}
placeholder="Karar metni, idare adı veya başvuran firma…"
className="w-full bg-slate-50 border border-slate-300 rounded-lg pl-9 pr-4 py-2.5 text-xs font-semibold"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
</div>
<button
onClick={() => ara()}
disabled={loading || !q.trim()}
className="bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white font-bold text-xs px-5 rounded-lg flex items-center gap-2"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />} Ara
</button>
</div>
{oneriler.length > 0 && (
<div className="flex gap-2 flex-wrap items-center">
<span className="text-[11px] text-slate-400 font-semibold">Takibinizdeki idareler:</span>
{oneriler.map((k) => (
<button key={k} onClick={() => { setQ(k); ara(k); }}
className="text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 px-2 py-1 rounded-md font-medium truncate max-w-[220px]">
{k}
</button>
))}
</div>
)}
</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">KİK kararlarına ulaşılamadı</p>
<p className="text-xs text-red-800 mt-0.5 leading-relaxed">{error}</p>
<p className="text-[11px] text-red-700 mt-1">
KİK sunucusu zaman zaman otomatik isteklere kapalı olabilir; birkaç dakika sonra tekrar deneyin.
</p>
</div>
</div>
)}
{data && (
<>
<div className="flex items-center justify-between gap-3 flex-wrap">
<p className="text-xs text-slate-500">
<strong className="text-slate-900">{data.totalCount?.toLocaleString('tr-TR') ?? data.results.length}</strong> karar bulundu
</p>
{data.kaynak && <span className="text-[11px] text-slate-400">Kaynak: {data.kaynak}</span>}
</div>
<div className="space-y-3">
{data.results.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 aramada karar bulunamadı</p>
</div>
)}
{data.results.map((k, i) => (
<div key={k.id || i} 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">
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
{k.kararNo && (
<span className="text-[10px] font-extrabold bg-amber-100 text-amber-900 px-2 py-0.5 rounded">
{k.kararNo}
</span>
)}
{k.kararTarihi && (
<span className="text-[11px] text-slate-500 flex items-center gap-1">
<Calendar className="w-3 h-3" /> {k.kararTarihi}
</span>
)}
{k.tur && <span className="text-[10px] bg-slate-100 text-slate-700 px-2 py-0.5 rounded font-bold">{k.tur}</span>}
</div>
<h3 className="font-bold text-sm text-slate-900 mt-1.5 leading-snug">{k.baslik || k.konu || 'Kurul Kararı'}</h3>
</div>
{k.link && (
<a href={k.link} target="_blank" rel="noopener noreferrer" className="text-amber-700 hover:text-amber-900 shrink-0" title="Kararı aç">
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
{(k.idare || k.basvuran) && (
<div className="flex items-center gap-3 flex-wrap text-[11px] text-slate-600">
{k.idare && <span className="flex items-center gap-1"><Building2 className="w-3 h-3 text-slate-400" /> {k.idare}</span>}
{k.basvuran && <span>Başvuran: <strong>{k.basvuran}</strong></span>}
</div>
)}
{k.ozet && <p className="text-xs text-slate-600 leading-relaxed line-clamp-3">{k.ozet}</p>}
</div>
))}
</div>
</>
)}
{!data && !loading && !error && (
<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">
Bir idare adı, firma veya konu yazıp arayın. KİK kurul kararları, benzer ihalelerde
hangi itirazların kabul/ret edildiğini gösterir — teklif ve şikâyet stratejinizi buna göre kurabilirsiniz.
</p>
</div>
)}
</div>
);
};
+148
View File
@@ -0,0 +1,148 @@
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { getToken } from '../../services/auth';
import { Check, Sparkles, CreditCard, Loader2, Info } from 'lucide-react';
export const Pricing = () => {
const { authUser } = useApp();
const [data, setData] = useState(null);
const [busy, setBusy] = useState(null);
const [error, setError] = useState(null);
const [yillik, setYillik] = useState(false);
useEffect(() => {
fetch('/api/billing/plans')
.then((r) => r.json())
.then(setData)
.catch(() => setData({ configured: false, plans: [] }));
}, []);
const upgrade = async (planId) => {
setBusy(planId);
setError(null);
try {
const res = await fetch('/api/billing/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ planId }),
});
const d = await res.json();
if (d.success && d.url) window.location.href = d.url;
else setError(d.error || 'Ödeme başlatılamadı');
} catch (e) {
setError(e.message);
} finally {
setBusy(null);
}
};
const plans = data?.plans || [];
const fiyatla = (p) => (yillik ? Math.round(p.fiyat * 12 * 0.83) : p.fiyat);
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-blue-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-blue-600 flex items-center justify-center shadow-md">
<CreditCard className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Planlar & Fiyatlandırma</h2>
<p className="text-xs text-blue-200 mt-0.5">İhtiyacınıza uygun planı seçin. İstediğiniz zaman iptal edebilirsiniz.</p>
</div>
</div>
</div>
{/* Aylık / Yıllık */}
<div className="flex justify-center">
<div className="bg-white border border-slate-200 rounded-xl p-1 inline-flex text-xs font-bold shadow-xs">
<button onClick={() => setYillik(false)} className={`px-4 py-2 rounded-lg transition-colors ${!yillik ? 'bg-blue-600 text-white' : 'text-slate-600 hover:bg-slate-50'}`}>
Aylık
</button>
<button onClick={() => setYillik(true)} className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-1.5 ${yillik ? 'bg-blue-600 text-white' : 'text-slate-600 hover:bg-slate-50'}`}>
Yıllık <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${yillik ? 'bg-amber-400 text-slate-900' : 'bg-emerald-100 text-emerald-800'}`}>%17 indirim</span>
</button>
</div>
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl p-3 text-xs text-red-800 text-center">{error}</div>
)}
{data && !data.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>Ödeme altyapısı henüz bağlanmadı.</strong> Satın alma için sunucuya <code className="bg-amber-100 px-1 rounded">STRIPE_SECRET_KEY</code> ve
plan fiyat kimlikleri (<code className="bg-amber-100 px-1 rounded">STRIPE_PRICE_PRO</code>, <code className="bg-amber-100 px-1 rounded">STRIPE_PRICE_KURUMSAL</code>) eklenmeli.
Planlar aşağıda görüntülenebilir.
</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{plans.map((p) => (
<div
key={p.id}
className={`rounded-2xl border p-6 flex flex-col gap-4 shadow-xs ${
p.vurgu ? 'border-blue-500 border-2 bg-linear-to-b from-blue-50 to-white shadow-lg' : 'border-slate-200 bg-white'
}`}
>
<div>
<div className="flex items-center justify-between gap-2">
<h3 className="font-black text-lg text-slate-900">{p.ad}</h3>
{p.rozet && (
<span className="bg-blue-600 text-white text-[10px] font-black px-2 py-0.5 rounded-full uppercase tracking-wide flex items-center gap-1">
<Sparkles className="w-3 h-3" /> {p.rozet}
</span>
)}
</div>
<p className="text-xs text-slate-500 mt-1">{p.ozet}</p>
</div>
<div>
<span className="text-3xl font-black text-slate-900">
{p.fiyat === 0 ? 'Ücretsiz' : `₺${fiyatla(p).toLocaleString('tr-TR')}`}
</span>
{p.fiyat > 0 && <span className="text-xs text-slate-500 font-medium"> /{yillik ? 'yıl' : 'ay'}</span>}
{p.fiyat > 0 && <p className="text-[11px] text-slate-400 mt-0.5">KDV hariç</p>}
</div>
<ul className="space-y-2 text-xs text-slate-700 flex-1">
{p.ozellikler.map((o, i) => (
<li key={i} className="flex items-start gap-2">
<Check className="w-3.5 h-3.5 text-emerald-600 shrink-0 mt-0.5" />
<span>{o}</span>
</li>
))}
</ul>
{p.fiyat === 0 ? (
<div className="text-center text-xs font-bold text-slate-500 bg-slate-50 border border-slate-200 rounded-xl py-2.5">
Mevcut planınız
</div>
) : (
<button
onClick={() => upgrade(p.id)}
disabled={!p.satinAlinabilir || busy === p.id || !authUser}
title={!authUser ? 'Önce giriş yapın' : !p.satinAlinabilir ? 'Ödeme altyapısı bağlanmadı' : ''}
className={`w-full font-extrabold text-xs py-3 rounded-xl flex items-center justify-center gap-2 transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
p.vurgu ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-slate-900 hover:bg-slate-800 text-white'
}`}
>
{busy === p.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
{busy === p.id ? 'Yönlendiriliyor…' : `${p.ad}'a Geç`}
</button>
)}
</div>
))}
</div>
<p className="text-[11px] text-slate-400 text-center leading-relaxed max-w-2xl mx-auto">
Ödemeler Stripe üzerinden güvenli şekilde alınır; kart bilgileriniz İhaleist sunucularında saklanmaz.
Aboneliğinizi istediğiniz zaman iptal edebilirsiniz.
</p>
</div>
);
};
+119
View File
@@ -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>
);
};
+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>
);
};