Files
ihaleist/src/components/tools/Pricing.jsx
T
hikmettanandClaude Fable 5.1 08c0a0af7e Kullanıcı testi hataları: sabit tarihler, kişisel varsayılanlar, kılavuz düğmesi, mobil başlık, AI taslak çökmesi
- Günlük Bülten ve İhale Takvimi sabit "10.08.2026" yerine bugünden hesaplar;
  takvim 7 günü gerçek ihale tarihleriyle sayar
- Ekip/üye/firma varsayılanları oturumdaki kullanıcıdan; sahte "Ekip Notları"
  ve sabit "%94 Uyum" rozeti kaldırıldı (gerçek uyum skoru varsa gösterilir)
- Kılavuz düğmesi document.body'ye portal (ekran dışına düşüyordu)
- Detay modalı havuzdaki güncel kaydı okur: Takip Et / Görev aç etiketleri güncellenir
- Mobil: başlıkta ikincil düğmeler md altında gizli, arama formu küçülebilir
- PWA sayfası: sahte önbellek sayıları yerine Cache API ve bellek sayımı
- Sözleşme sorgula pazar payı bir ondalık (hep %0 görünüyordu)
- Rakip Radar "SMS" vaadi düzeltildi; sektör=CPV etiketi tekrarı gizlendi;
  Stripe kurulum uyarısı yalnız yöneticiye
- Açılışta 7 gereksiz PUT /api/me/data: ilk yükleme yazımı atlanır
- server: teklif taslağı çökmesi (kalemOku nesne döndürür), denetim yanıtından
  model adı kaldırıldı, gereklilikler satır bazlı; derin analiz 12k token +
  kesik JSON onarımı

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 09:43:41 +03:00

218 lines
10 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { getToken } from '../../services/auth';
import { Check, Sparkles, CreditCard, Loader2, Info, ShieldCheck, XCircle } from 'lucide-react';
import { CancelSubscriptionModal } from '../CancelSubscriptionModal';
export const Pricing = () => {
const { authUser, plan, planYenile, isAdmin } = useApp();
const [data, setData] = useState(null);
const [busy, setBusy] = useState(null);
const [error, setError] = useState(null);
const [yillik, setYillik] = useState(true); // yıllık varsayılan (indirimli)
const [iptalAcik, setIptalAcik] = useState(false);
const [iptalSonuc, setIptalSonuc] = useState(null);
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, periyot: yillik ? 'yillik' : 'aylik' }),
});
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 || [];
// Yıllık tutar Stripe'taki fiyatla birebir aynı olsun diye plandan gelir
const fiyatla = (p) => (yillik ? (p.fiyatYillik ?? Math.round(p.fiyat * 12 * 0.83)) : p.fiyat);
const aylikKarsiligi = (p) => (p.fiyatYillik ? Math.round(p.fiyatYillik / 12) : 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(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>
<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>
</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 && isAdmin && (
<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>
)}
{/* Mevcut abonelik — gerçek /api/me/plan verisi */}
{plan && (
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-xs">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-emerald-50 border border-emerald-200 flex items-center justify-center">
<ShieldCheck className="w-5 h-5 text-emerald-600" />
</div>
<div>
<p className="text-xs text-slate-500">Mevcut aboneliğiniz</p>
<p className="font-black text-slate-900">
{plan.abonelikVar ? plan.abonelikAdi : 'Ücretsiz'}
{plan.yonetici && (
<span className="ml-2 text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-200">
yönetici erişimi: {plan.planAdi}
</span>
)}
</p>
</div>
</div>
{plan.iptalEdilebilir && (
<button
onClick={() => setIptalAcik(true)}
className="text-xs font-bold text-red-600 hover:text-red-700 hover:bg-red-50 border border-red-200 px-3 py-2 rounded-xl flex items-center gap-1.5 transition-colors"
>
<XCircle className="w-4 h-4" /> Aboneliği iptal et
</button>
)}
</div>
{plan.yonetici && (
<p className="text-[11px] text-slate-400 mt-2 leading-relaxed">
Yönetici hesapları tüm özelliklere erişir; bu, satın aldığınız aboneliğin yerine geçmez.
</p>
)}
{iptalSonuc && (
<div className="mt-3 bg-amber-50 border border-amber-200 rounded-xl p-3 text-xs text-amber-900">
Aboneliğiniz iptal edildi. Erişiminiz{' '}
<strong>{iptalSonuc.bitis ? new Date(iptalSonuc.bitis).toLocaleDateString('tr-TR') : 'dönem sonuna'}</strong>{' '}
kadar açık kalacak, sonrasında yenileme yapılmayacak.
</div>
)}
</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ç
{yillik && (
<> · ayda ₺{aylikKarsiligi(p).toLocaleString('tr-TR')} · yılda{' '}
<span className="text-emerald-600 font-bold">
₺{(p.fiyat * 12 - fiyatla(p)).toLocaleString('tr-TR')} tasarruf
</span>
</>
)}
</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={!(yillik ? p.satinAlinabilirYillik : p.satinAlinabilir) || busy === p.id || !authUser}
title={!authUser ? 'Önce giriş yapın' : !(yillik ? p.satinAlinabilirYillik : 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>
{iptalAcik && (
<CancelSubscriptionModal
planAdi={plan?.abonelikAdi || 'Mevcut'}
nedenler={data?.iptalNedenleri || []}
onKapat={() => setIptalAcik(false)}
onIptalEdildi={(out) => { setIptalAcik(false); setIptalSonuc(out); planYenile(); }}
/>
)}
<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>
);
};