fix: persist weekly brief preference and expose request failures

This commit is contained in:
2026-09-06 14:20:31 +03:00
parent 9db12a5eae
commit b2a416c908
7 changed files with 92 additions and 57 deletions
+40 -47
View File
@@ -1,48 +1,41 @@
import React, { useEffect, useState } from 'react';
import { CalendarClock, Eye, Loader2 } from 'lucide-react';
import { getToken } from '../../services/auth';
/**
* Haftalık brief tercihi (Pazartesi 08:00): bu hafta yapılacak takipli
* ihaleler + geçen hafta sonuçlanan takipliler — Telegram ve e-posta.
* "Önizle" şu anki içeriği gösterir; anahtar kapatır.
*/
const iste = (opts = {}) => fetch('/api/me/brief', { headers: { Authorization: `Bearer ${getToken()}`, 'Content-Type': 'application/json' }, ...opts }).then((r) => r.json());
export const BriefTercihi = () => {
const [kapali, setKapali] = useState(false);
const [metin, setMetin] = useState(null);
const [mesgul, setMesgul] = useState(false);
useEffect(() => { iste().then((d) => { if (d?.success) setKapali(!!d.kapali); }).catch(() => {}); }, []);
const degistir = async () => {
const yeni = !kapali; setKapali(yeni);
try { await iste({ method: 'POST', body: JSON.stringify({ kapali: yeni }) }); } catch { setKapali(!yeni); }
};
const onizle = async () => {
setMesgul(true);
try { const d = await iste(); setMetin(d?.metin || 'Önizleme alınamadı.'); } catch { setMetin('Önizleme alınamadı.'); } finally { setMesgul(false); }
};
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-3">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2"><CalendarClock className="w-4 h-4 text-amber-500" /> Haftalık Brief</h3>
<p className="text-[11px] text-slate-500 mt-1">Her Pazartesi 08:00: bu hafta yapılacak takipli ihaleler ve geçen hafta sonuçlanan takipliler — Telegram (bağlıysa) ve e-posta ile tek mesaj.</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={onizle} disabled={mesgul} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">
{mesgul ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />} Önizle
</button>
<button role="switch" aria-checked={!kapali} onClick={degistir}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition ${kapali ? 'bg-slate-300' : 'bg-emerald-500'}`}>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition ${kapali ? 'translate-x-0.5' : 'translate-x-4'}`} />
</button>
</div>
</div>
{metin && <pre className="whitespace-pre-wrap rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700 font-sans leading-relaxed">{metin}</pre>}
</div>
);
import React,{useEffect,useRef,useState} from 'react';
import {CalendarClock} from 'lucide-react';
import {getToken} from '../../services/auth';
import {useApp} from '../../context/AppContext';
import {getWeeklyBrief,saveWeeklyBrief} from '../../services/weeklyBrief';
export const BriefTercihi=()=>{
const {authUser}=useApp();
return <BriefPanel key={`${authUser?.email}:${getToken()}`}/>;
};
function BriefPanel(){
const [value,setValue]=useState(null),[metin,setMetin]=useState(null),[busy,setBusy]=useState(true),[error,setError]=useState(null),[attempt,setAttempt]=useState(0);
const lifetime=useRef(null),lock=useRef(false);
useEffect(()=>{
const controller=new AbortController();lifetime.current=controller;lock.current=true;setBusy(true);setError(null);
getWeeklyBrief({signal:controller.signal}).then(data=>{if(!controller.signal.aborted)setValue(data);})
.catch(e=>{if(!controller.signal.aborted)setError(e.message);})
.finally(()=>{if(!controller.signal.aborted){lock.current=false;setBusy(false);}});
return()=>controller.abort();
},[attempt]);
const run=async preview=>{
if(lock.current||!value)return;
const signal=lifetime.current.signal;lock.current=true;setBusy(true);setError(null);
try{
const data=preview?await getWeeklyBrief({preview:true,signal}):await saveWeeklyBrief(!value.kapali,value.revision,{signal});
if(!signal.aborted){setValue(data);if(preview)setMetin(data.metin);}
}catch(e){if(!signal.aborted)setError(e.message);}
finally{if(!signal.aborted){lock.current=false;setBusy(false);}}
};
return <section aria-label="Haftalık özet ayarı" className="bg-white rounded-2xl border border-slate-200 p-5 space-y-3">
<h3 className="font-bold text-sm flex gap-2"><CalendarClock className="w-4 h-4"/>Haftalık Brief</h3>
<p className="text-xs text-slate-500">Haftanın takipli ihaleleri ve geçen hafta sonuçlanan takipliler için özet tercihi. Gönderim, sunucu zamanlayıcısının ve ilgili e-posta/Telegram hizmetinin etkin olmasına bağlıdır.</p>
<div className="flex gap-3 flex-wrap">
{value&&<button role="switch" aria-label="Haftalık özet gönderimi" aria-checked={!value.kapali} disabled={busy} onClick={()=>run(false)} className="border rounded px-3 py-2 text-xs">{value.kapali?'Haftalık özet kapalı':'Haftalık özet açık'}</button>}
<button disabled={busy||!value} onClick={()=>run(true)} className="border rounded px-3 py-2 text-xs">Önizle</button>
<button disabled={busy} onClick={()=>setAttempt(n=>n+1)} className="border rounded px-3 py-2 text-xs">Özet ayarını yenile</button>
</div>
{busy&&<p role="status" className="text-xs">Haftalık özet işlemi sürüyor…</p>}
{error&&<p role="alert" className="text-xs text-red-700">{error}</p>}
{metin!==null&&<pre className="whitespace-pre-wrap rounded border bg-slate-50 p-3 text-xs">{metin}</pre>}
</section>;
}