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