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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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' }] : []),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user