Expose partial item search failures and allow query-scoped retries

This commit is contained in:
2026-09-06 10:31:58 +03:00
parent 0c16bbd96f
commit 21e80d0dd6
5 changed files with 53 additions and 12 deletions
+6 -1
View File
@@ -16,7 +16,7 @@ export const TenderList = () => {
const {
tenders,
searchQuery,
kalemEslesen,
kalemEslesen, itemSearchState, retryItemSearch,
uyumEsigi,
selectedCity,
selectedAlimTuru,
@@ -174,6 +174,11 @@ export const TenderList = () => {
</div>
{sortBy === 'newest' && <p className="text-xs text-slate-500">Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.</p>}
{itemSearchState?.status === 'loading' && <p role="status" className="text-xs text-slate-600">Doküman kalemlerinde arama sürüyor; listedeki sonuçlar henüz genişleyebilir.</p>}
{itemSearchState?.status === 'error' && <div role="alert" className="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900">
<p>{itemSearchState.error} Başlık, kurum ve yüklü kart bilgileriyle arama sürüyor; sonuçlar eksik olabilir.</p>
<button type="button" onClick={retryItemSearch} className="mt-1 underline font-semibold">Kalem aramasını yeniden dene</button>
</div>}
{preparedExport && preparedExport.session === getToken() && <div role="status" className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs space-y-1">
<p>{preparedExport.createdAt} itibarıyla {preparedExport.count} ihale için Excel hazır{preparedExport.query ? ` — arama: ${preparedExport.query}` : ''}.</p>
<a href={preparedExport.url} download={preparedExport.name} className="font-bold text-emerald-800 underline">Excel dosyasını indir — {preparedExport.name}</a>
+11 -6
View File
@@ -58,21 +58,26 @@ export const AppProvider = ({ children }) => {
// ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor.
const [kalemSonucu, setKalemSonucu] = useState(null);
const kalemEslesen = matchingItemResults(kalemSonucu, searchQuery);
const [kalemRetry,setKalemRetry] = useState(0);
const retryItemSearch = () => setKalemRetry(n=>n+1);
const itemSearchState = kalemSonucu?.query === normalizeTenderSearch(searchQuery) ? kalemSonucu : null;
useEffect(() => {
const q = searchQuery.trim();
if (q.length < 2) { setKalemSonucu(null); return; }
let iptal = false;
const controller=new AbortController();
setKalemSonucu({query:normalizeTenderSearch(q),status:'loading',matches:null});
const zaman = setTimeout(() => {
import('../services/kalem')
.then(({ kalemAra }) => kalemAra(q))
.then(({ kalemAra }) => kalemAra(q,{signal:controller.signal}))
.then((sonuclar) => {
if (iptal) return;
setKalemSonucu({query:normalizeTenderSearch(q),matches:sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null});
setKalemSonucu({query:normalizeTenderSearch(q),status:'loaded',matches:sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null});
})
.catch(() => { if (!iptal) setKalemSonucu(null); });
.catch(error => { if (!iptal) setKalemSonucu({query:normalizeTenderSearch(q),status:'error',error:error.message,matches:null}); });
}, 350);
return () => { iptal = true; clearTimeout(zaman); };
}, [searchQuery]);
return () => { iptal = true; controller.abort(); clearTimeout(zaman); };
}, [searchQuery,kalemRetry]);
const [selectedCity, setSelectedCity] = useState('');
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
const [selectedKurumTipi, setSelectedKurumTipi] = useState('');
@@ -725,7 +730,7 @@ export const AppProvider = ({ children }) => {
handleLogout,
liveStats,
searchQuery,
kalemEslesen,
kalemEslesen, itemSearchState, retryItemSearch,
setSearchQuery,
selectedCity,
setSelectedCity,
+15 -5
View File
@@ -26,11 +26,21 @@ export async function kalemleriGetir(ikn, {signal, timeoutMs = 30000} = {}) {
}
/** Kalem metninde arama — eşleşen İKN'ler. */
export async function kalemAra(q) {
const r = await fetch(`/api/kalem/ara?q=${encodeURIComponent(q)}`);
const d = await r.json().catch(() => ({}));
if (!d.success) return [];
return d.sonuclar || [];
export async function kalemAra(q,{signal,timeoutMs=30000}={}) {
const controller=new AbortController();
const cancel=()=>controller.abort();
if(signal?.aborted)cancel();else signal?.addEventListener('abort',cancel,{once:true});
let timedOut=false;
const timer=setTimeout(()=>{timedOut=true;controller.abort();},timeoutMs);
try {
const r=await fetch(`/api/kalem/ara?q=${encodeURIComponent(q)}`,{signal:controller.signal});
const d=await r.json().catch(()=>null);
if(!r.ok || d?.success!==true || !Array.isArray(d.sonuclar))throw new Error('Doküman kalemlerinde arama tamamlanamadı.');
return d.sonuclar;
} catch(error) {
if(timedOut)throw new Error('Doküman kalemlerinde arama zaman aşımına uğradı.');
throw error;
} finally {clearTimeout(timer);signal?.removeEventListener('abort',cancel);}
}
/**