Files
ihaleist/src/components/tools/NotifySubscriptions.jsx
T

262 lines
13 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, useRef } from 'react';
import { useApp } from '../../context/AppContext';
import { TelegramBaglanti } from './TelegramBaglanti';
import { BriefTercihi } from './BriefTercihi';
import { getToken } from '../../services/auth';
import {accountRequest,requireArray} from '../../services/accountRequest';
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}=useApp();
return authUser ? <SubscriptionForm key={`${authUser.email}:${getToken()}`} /> : null;
};
const SubscriptionForm = () => {
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 });
const alive=useRef(true),mutation=useRef(false),scan=useRef(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=async()=>{
if(mutation.current)return;
mutation.current=true;setBusy(true);setError(null);
try {const data=await accountRequest('/api/notify/subscriptions');const rows=requireArray(data,'abonelikler');if(alive.current)setAbonelikler(rows);}
catch(e){if(alive.current)setError(e.message);}
finally {mutation.current=false;if(alive.current)setBusy(false);}
};
useEffect(()=>{alive.current=true;void yukle();return()=>{alive.current=false;};},[]);
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()=>{
if(scan.current)return;scan.current=true;setTaraniyor(true);setTaramaHata(null);setTarama(null);
try {
const data=await accountRequest('/api/company/scan',{method:'POST',timeoutMs:120000,body:JSON.stringify({url:site})});
if(!Number.isFinite(data.karakter)||!Number.isFinite(data.tarananSayfa)||!Array.isArray(data.sonuc?.ihaleKonulari))throw new Error('Site tarama sonucu geçersiz.');
if(alive.current)setTarama(data);
}catch(e){if(alive.current)setTaramaHata(e.message);}
finally {scan.current=false;if(alive.current)setTaraniyor(false);}
};
const kaydet=async()=>{
if(mutation.current)return;
if(!konular.length){setError('En az bir konu seçin.');return;}
mutation.current=true;setBusy(true);setError(null);
try {
const data=await accountRequest('/api/notify/subscriptions',{method:'POST',body:JSON.stringify({kriter:{konular,sehir:daralt.sehir,alimTuru:daralt.alimTuru,kurumlar:daralt.favoriKurumlar?favoriteKurumlar:[]}})});
const rows=requireArray(data,'abonelikler');
if(alive.current){setAbonelikler(rows);setKonular([]);setDaralt({sehir:'',alimTuru:'',favoriKurumlar:false});}
}catch(e){if(alive.current)setError(e.message);}
finally {mutation.current=false;if(alive.current)setBusy(false);}
};
const sil=async id=>{
if(mutation.current)return;mutation.current=true;setBusy(true);setError(null);
try {
const data=await accountRequest('/api/notify/subscriptions?'+new URLSearchParams({id}),{method:'DELETE'});
const rows=requireArray(data,'abonelikler');if(alive.current)setAbonelikler(rows);
}catch(e){if(alive.current)setError(e.message);}
finally {mutation.current=false;if(alive.current)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,
E-posta gönderim hizmeti etkin olduğunda, eşleşen yeni ihaleler <strong>{authUser.email}</strong> adresine gönderilir.
</p>
{/* Mevcut kayıtlar */}
{abonelikler === null ? (!error && (
<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'}
{a.deliveryStatus && <span className="block text-amber-700">{a.deliveryStatus.state==='uncertain'?'Gönderim sonucu belirsiz. Uygulama yöneticisinin sağlayıcı kaydını kontrol etmesi gerekiyor.':'E-posta gönderimi bekliyor.'}</span>}
</p>
</div>
<button onClick={() => sil(a.id)} disabled={busy} className="text-slate-400 hover:text-red-500 shrink-0 mt-0.5" aria-label={`Aboneliği sil: ${ozet(a)}`} 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 || abonelikler===null || !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 role="alert" 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} <button disabled={busy} onClick={yukle} className="underline">Abonelikleri yeniden yükle</button></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. Sonuçlanmış arşiv kayıtları bildirime girmez. Aboneliği kaydetmek, e-postanın teslim edildiği anlamına gelmez.
</p>
</div>
</div>
</>
);
};