Files
ihaleist/src/components/tools/NotifySubscriptions.jsx
T
hikmettanandClaude Fable 5.1 48f0c48732 feat: İhaleAB'den taşınan özellikler — MCP sunucusu, RSS, haftalık brief, teklif asistanı, yeterlilik rozeti, 5 yeni kırmızı bayrak
- /mcp (JSON-RPC, ih_live_ anahtarı): search_tenders, get_tender, search_results, search_items, upcoming_deadlines
- /api/feeds/tenders.rss (anahtarlı) ve /api/feeds/sonuc.rss (açık)
- Haftalık brief (Pazartesi 08:00, Telegram + e-posta), /api/me/brief tercihi + önizleme, sağ panelde yaklaşan ihale tarihleri
- Detayda Teklif Asistanı sekmesi: belgeye sor (atıflı), taslak denetimi, taslak üretimi (analyzeSpec zinciri)
- Derin analiz belge listesi × Doküman Deposu eşleştirme rozeti
- Kırmızı bayrak: kısa süre, hafta sonu tarih, geç sözleşme, kazananı gizli, DT parçalama şüphesi
- Kullanım kılavuzu güncellendi

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

277 lines
12 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 { TelegramBaglanti } from './TelegramBaglanti';
import { BriefTercihi } from './BriefTercihi';
import { getToken } from '../../services/auth';
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
/**
* Otomatik e-posta bildirimi — KONU odaklı.
* Kullanıcı hangi konularda ihale alabildiğini seçer; il/kurum gibi daraltmalar
* isteğe bağlıdır (varsayılan: tüm Türkiye).
*/
export const NotifySubscriptions = () => {
const { authUser, favoriteKurumlar, userProfile, sektorListesi } = useApp();
const [abonelikler, setAbonelikler] = useState(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [konular, setKonular] = useState([]);
const [ozelKonu, setOzelKonu] = useState('');
const [gelismis, setGelismis] = useState(false);
const [daralt, setDaralt] = useState({ sehir: '', alimTuru: '', favoriKurumlar: false });
// Site tarama
const [site, setSite] = useState(userProfile?.website || '');
const [tarama, setTarama] = useState(null);
const [taraniyor, setTaraniyor] = useState(false);
const [taramaHata, setTaramaHata] = useState(null);
const yukle = () => {
if (!authUser) return;
fetch('/api/notify/subscriptions', { headers: { Authorization: `Bearer ${getToken()}` } })
.then((r) => r.json())
.then((d) => setAbonelikler(d.abonelikler || []))
.catch(() => setAbonelikler([]));
};
useEffect(yukle, [authUser]);
const konuEkle = (k) => {
const t = String(k || '').trim();
if (!t || konular.some((x) => x.toLocaleLowerCase('tr-TR') === t.toLocaleLowerCase('tr-TR'))) return;
setKonular([...konular, t]);
};
const konuCikar = (k) => setKonular(konular.filter((x) => x !== k));
const siteyiTara = async () => {
setTaraniyor(true); setTaramaHata(null); setTarama(null);
try {
const res = await fetch('/api/company/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ url: site }),
});
const d = await res.json();
if (!d.success) throw new Error(d.error);
setTarama(d);
} catch (e) {
setTaramaHata(e.message);
} finally { setTaraniyor(false); }
};
const kaydet = async () => {
if (!konular.length) { setError('En az bir konu seçin.'); return; }
setBusy(true); setError(null);
try {
const res = await fetch('/api/notify/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({
kriter: {
konular,
sehir: daralt.sehir,
alimTuru: daralt.alimTuru,
kurumlar: daralt.favoriKurumlar ? favoriteKurumlar : [],
},
}),
});
const d = await res.json();
if (!d.success) throw new Error(d.error);
setAbonelikler(d.abonelikler);
setKonular([]); setDaralt({ sehir: '', alimTuru: '', favoriKurumlar: false });
} catch (e) { setError(e.message); }
finally { setBusy(false); }
};
const sil = async (id) => {
setBusy(true);
try {
const res = await fetch(`/api/notify/subscriptions?id=${id}`, {
method: 'DELETE', headers: { Authorization: `Bearer ${getToken()}` },
});
const d = await res.json();
setAbonelikler(d.abonelikler || []);
} catch (e) { setError(e.message); }
finally { setBusy(false); }
};
const ozet = (a) => [
a.konular?.length ? a.konular.join(' · ') : null,
a.sehir && `📍 ${a.sehir}`,
a.alimTuru && `(${a.alimTuru})`,
a.kurumlar?.length && `${a.kurumlar.length} favori kurum`,
].filter(Boolean).join(' ') || 'Tüm ihaleler';
if (!authUser) return null;
const oneriler = tarama?.sonuc?.ihaleKonulari || [];
return (
<>
<BriefTercihi />
<TelegramBaglanti />
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-5">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<BellRing className="w-4 h-4 text-emerald-600" /> Otomatik E-posta Bildirimi
</h3>
{abonelikler && (
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full">
{abonelikler.length} kayıt
</span>
)}
</div>
<p className="text-xs text-slate-600 leading-relaxed">
Hangi <strong>konularda</strong> ihale alabiliyorsanız onları seçin — <strong>tüm Türkiye</strong> taranır,
yalnızca size uyan yeni ihaleler <strong>{authUser.email}</strong> adresine gelir. Gereksiz ilan gönderilmez.
</p>
{/* Mevcut kayıtlar */}
{abonelikler === null ? (
<div className="flex items-center gap-2 text-xs text-slate-400"><Loader2 className="w-3.5 h-3.5 animate-spin" /> yükleniyor…</div>
) : abonelikler.length > 0 && (
<div className="space-y-2">
{abonelikler.map((a) => (
<div key={a.id} className="flex items-start justify-between gap-2 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2">
<div className="min-w-0">
<p className="text-xs font-bold text-slate-800">{ozet(a)}</p>
<p className="text-[10px] text-slate-400 mt-0.5">
{a.sonGonderim ? `son gönderim: ${new Date(a.sonGonderim).toLocaleString('tr-TR')}` : 'ilk tur bekleniyor'}
</p>
</div>
<button onClick={() => sil(a.id)} disabled={busy} className="text-slate-400 hover:text-red-500 shrink-0 mt-0.5" title="Sil">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
{/* Site tarama → konu önerisi */}
<div className="bg-blue-50/60 border border-blue-200 rounded-xl p-3.5 space-y-2.5">
<p className="text-[11px] font-bold text-blue-900 flex items-center gap-1.5">
<Globe className="w-3.5 h-3.5" /> Sitenizi tarayıp konuları otomatik önerelim
</p>
<div className="flex gap-2">
<input value={site} onChange={(e) => setSite(e.target.value)}
placeholder="firmasitesi.com"
className="flex-1 bg-white border border-blue-200 rounded-lg px-2.5 py-2 text-xs" />
<button onClick={siteyiTara} disabled={taraniyor || !site.trim()}
className="bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white font-bold text-xs px-3.5 rounded-lg flex items-center gap-1.5">
{taraniyor ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Sparkles className="w-3.5 h-3.5" />}
{taraniyor ? 'Taranıyor…' : 'Tara'}
</button>
</div>
{taramaHata && <p className="text-[11px] text-red-700">⚠ {taramaHata}</p>}
{tarama && (
<div className="space-y-2">
<p className="text-[11px] text-blue-900">
<strong>{tarama.tarananSayfa} sayfa</strong> tarandı ({tarama.karakter.toLocaleString('tr-TR')} karakter).
{tarama.sonuc?.ozet && <> {tarama.sonuc.ozet}</>}
</p>
{oneriler.length > 0 && (
<>
<p className="text-[10px] text-blue-700 font-semibold">Önerilen konular — eklemek için tıklayın:</p>
<div className="flex flex-wrap gap-1.5">
{oneriler.map((k) => (
<button key={k} onClick={() => konuEkle(k)}
className="text-[11px] bg-white hover:bg-blue-100 border border-blue-300 text-blue-900 px-2 py-1 rounded-md font-medium">
+ {k}
</button>
))}
</div>
</>
)}
</div>
)}
</div>
{/* Seçilen konular */}
<div className="space-y-2">
<p className="text-[11px] font-bold text-slate-700 uppercase tracking-wide">Seçilen konular</p>
{konular.length === 0 ? (
<p className="text-[11px] text-slate-400">Henüz konu seçilmedi.</p>
) : (
<div className="flex flex-wrap gap-1.5">
{konular.map((k) => (
<span key={k} className="text-[11px] bg-emerald-50 border border-emerald-300 text-emerald-900 px-2 py-1 rounded-md font-semibold flex items-center gap-1">
{k}
<button onClick={() => konuCikar(k)} className="text-emerald-700 hover:text-red-600"><X className="w-3 h-3" /></button>
</span>
))}
</div>
)}
<div className="flex gap-2 pt-1">
<input value={ozelKonu} onChange={(e) => setOzelKonu(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { konuEkle(ozelKonu); setOzelKonu(''); } }}
placeholder="Kendi konunuzu yazın ve Enter'a basın"
className="flex-1 bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-2 text-xs" />
<button onClick={() => { konuEkle(ozelKonu); setOzelKonu(''); }}
className="bg-slate-800 hover:bg-slate-700 text-white font-bold text-xs px-3 rounded-lg">Ekle</button>
</div>
<p className="text-[10px] text-slate-400 pt-1">Hazır sektörler:</p>
<div className="flex flex-wrap gap-1.5">
{sektorListesi.map((s) => (
<button key={s} onClick={() => konuEkle(s)}
className="text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 px-2 py-1 rounded-md">
+ {s}
</button>
))}
</div>
</div>
{/* Gelişmiş daraltma (isteğe bağlı) */}
<div>
<button onClick={() => setGelismis(!gelismis)} className="text-[11px] font-bold text-slate-500 hover:text-slate-800 flex items-center gap-1">
{gelismis ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
Daha da daralt (isteğe bağlı) — varsayılan: tüm Türkiye
</button>
{gelismis && (
<div className="grid grid-cols-2 gap-2 mt-2">
<input value={daralt.sehir} onChange={(e) => setDaralt({ ...daralt, sehir: e.target.value })}
placeholder="Yalnızca bu il" className="bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-2 text-xs" />
<input value={daralt.alimTuru} onChange={(e) => setDaralt({ ...daralt, alimTuru: e.target.value })}
placeholder="Alım türü (ör. Doğrudan Temin)" className="bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-2 text-xs" />
{favoriteKurumlar?.length > 0 && (
<label className="col-span-2 flex items-center gap-2 text-xs text-slate-700 cursor-pointer">
<input type="checkbox" checked={daralt.favoriKurumlar}
onChange={(e) => setDaralt({ ...daralt, favoriKurumlar: e.target.checked })}
className="w-3.5 h-3.5 accent-emerald-600 rounded" />
Yalnızca favori kurumlarım ({favoriteKurumlar.length})
</label>
)}
</div>
)}
</div>
<button onClick={kaydet} disabled={busy || !konular.length}
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-2">
{busy ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
Bildirimi Kaydet
</button>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-red-800">{error}</p>
</div>
)}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3 flex gap-2">
<Info className="w-3.5 h-3.5 text-slate-500 shrink-0 mt-0.5" />
<p className="text-[11px] text-slate-600 leading-relaxed">
İlk kayıtta mevcut ihaleler <strong>bildirilmez</strong> — yalnızca temel alınır, sonrasında çıkan
yeni ihaleler gönderilir. Aynı ihale iki kez gönderilmez; sonuçlanmış arşiv kayıtları bildirime girmez.
</p>
</div>
</div>
</>
);
};