Open and retain tender details discovered outside the main pool
This commit is contained in:
@@ -105,6 +105,7 @@ export const TenderDetailModal = () => {
|
||||
<div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={dialogTitleId} tabIndex={-1} onKeyDown={handleDialogKey} className="bg-white w-full max-w-6xl rounded-2xl shadow-2xl border border-slate-200 overflow-hidden my-auto flex flex-col max-h-[92vh]">
|
||||
|
||||
{(actionError || saveError) && <p role="alert" className="p-3 bg-red-50 text-red-900">{actionError || saveError}</p>}
|
||||
{tender.sourceWarning && <p role="alert" className="p-3 bg-amber-50 text-amber-900 text-sm">{tender.sourceWarning}</p>}
|
||||
{detailLoad?.id === tender.id && detailLoad.status === 'loading' && <p role="status" className="p-3 bg-blue-50 text-blue-900">Kaynak ilan metni yükleniyor…</p>}
|
||||
{detailLoad?.id === tender.id && detailLoad.status === 'error' && <div role="alert" className="p-3 bg-amber-50 text-amber-900 text-sm">
|
||||
<p>{detailLoad.error}</p>
|
||||
|
||||
@@ -24,7 +24,7 @@ const gecmisMi = tarihSaat => {
|
||||
|
||||
export const UrunAramaModal = () => {
|
||||
const {
|
||||
isUrunAramaOpen, setIsUrunAramaOpen, setActiveTenderDetail, tenders,
|
||||
isUrunAramaOpen, setIsUrunAramaOpen, setActiveTenderDetail, openSearchTender, tenders,
|
||||
urunAramaBaslangic, setUrunAramaBaslangic,
|
||||
} = useApp();
|
||||
const modal = useRef(null);
|
||||
@@ -300,12 +300,12 @@ export const UrunAramaModal = () => {
|
||||
<span className="flex items-center gap-1"><CalendarDays className="w-3 h-3 text-blue-600" />{t.ihaleTarihSaat}</span>
|
||||
)}
|
||||
<span className="text-slate-400">{t.durum}</span>
|
||||
{acik && (
|
||||
{/^\d{4}\/\d+$/.test(String(t.ikn || '')) && (
|
||||
<button
|
||||
onClick={() => { setActiveTenderDetail(acik); setIsUrunAramaOpen(false); }}
|
||||
onClick={() => { openSearchTender(t); setIsUrunAramaOpen(false); }}
|
||||
className="font-bold text-indigo-700 hover:underline"
|
||||
>
|
||||
Havuzda açık — detayı aç
|
||||
{acik ? 'Listedeki detayı aç' : 'İhale detayını aç'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { searchTender } from '../services/searchTender';
|
||||
import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch';
|
||||
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
|
||||
@@ -281,6 +282,14 @@ export const AppProvider = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
const [activeTenderDetail, setActiveTenderDetail] = useState(null);
|
||||
const openSearchTender = (result) => {
|
||||
const existing=tenders.find(t=>t.ikn===result.ikn);
|
||||
const candidate=existing || searchTender(result);
|
||||
const merged=applyTenderStates([candidate],savedTenderStates.current).find(t=>t.ikn===candidate.ikn) || candidate;
|
||||
if(!existing) setTenders(previous=>previous.some(t=>t.ikn===merged.ikn) ? previous : [...previous,merged]);
|
||||
setActiveTenderDetail(merged);
|
||||
};
|
||||
|
||||
const [detailLoad, setDetailLoad] = useState(null);
|
||||
const [detailRetry, setDetailRetry] = useState(0);
|
||||
const retryTenderDetail = () => setDetailRetry(value => value + 1);
|
||||
@@ -288,7 +297,7 @@ export const AppProvider = ({ children }) => {
|
||||
// Canlı ihalede detay açıldığında ilan metnini API'den tamamla
|
||||
useEffect(() => {
|
||||
const t = activeTenderDetail;
|
||||
if (!t || !t.id?.startsWith('ilan-') || t.aciklama) { setDetailLoad(null); return; }
|
||||
if (!t || !(t.id?.startsWith('ilan-') || t.id?.startsWith('ekap-')) || t.aciklama) { setDetailLoad(null); return; }
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
setDetailLoad({id:t.id,status:'loading'});
|
||||
@@ -744,6 +753,7 @@ export const AppProvider = ({ children }) => {
|
||||
addSavedFilter,
|
||||
deleteSavedFilter,
|
||||
activeTenderDetail,
|
||||
openSearchTender,
|
||||
detailLoad,
|
||||
retryTenderDetail,
|
||||
setActiveTenderDetail,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Keep unknown fields unknown when promoting a document-search result to a detail. */
|
||||
export function searchTender(result) {
|
||||
const ikn=String(result.ikn || '').trim();
|
||||
if(!/^\d{4}\/\d+$/.test(ikn)) throw new Error('Bu sonuç için geçerli EKAP İKN bilgisi yok.');
|
||||
const match=String(result.ihaleTarihSaat || '').trim().match(/^(\d{2}\.\d{2}\.\d{4})(?:\s+(\d{2}:\d{2}))?/);
|
||||
return {id:`ekap-${ikn}`,ikn,baslik:result.baslik || '',kurum:result.kurum || '',sehir:result.sehir || '',
|
||||
tarih:match?.[1] || '',saat:match?.[2] || '',alimTuru:result.tur || '',usul:result.usul || '',
|
||||
durum:result.durum || '',items:[],aciklama:'',kaynak:'EKAP ürün araması',
|
||||
sourceDate:result.ihaleTarihSaat || '',discoveredFromSearch:true};
|
||||
}
|
||||
@@ -20,10 +20,17 @@ export async function fetchTenderDetail(tenderId, {signal, timeoutMs = 30000} =
|
||||
let timedOut = false;
|
||||
const timer = setTimeout(()=>{timedOut=true;controller.abort();},timeoutMs);
|
||||
try {
|
||||
const res = await fetch(`/api/tenders/${encodeURIComponent(tenderId)}/detail`, {signal:controller.signal});
|
||||
const searchResult=String(tenderId).startsWith('ekap-');
|
||||
const url=searchResult ? `/api/documents/ilan?ikn=${encodeURIComponent(tenderId.slice(5))}` : `/api/tenders/${encodeURIComponent(tenderId)}/detail`;
|
||||
const res = await fetch(url, {signal:controller.signal});
|
||||
if (!res.ok) throw new Error(`Detay API hatası: HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.error || 'Detay alınamadı');
|
||||
if (searchResult) {
|
||||
if (data.ikn !== tenderId.slice(5)) throw new Error('Kaynak yanıtındaki İKN aranan ihaleyle uyuşmuyor.');
|
||||
data.aciklama=data.metin;
|
||||
data.sourceWarning=data.ilanSayisi ? null : 'EKAP ilan metni bulunamadı; yalnız temel ihale bilgileri alındı. Resmi dokümanı kontrol edin.';
|
||||
}
|
||||
if (typeof data.aciklama !== 'string' || !data.aciklama.trim())
|
||||
throw new Error('Kaynak ilan metni alınamadı. Yeniden deneyin veya asıl ilanı kontrol edin.');
|
||||
return data;
|
||||
|
||||
Reference in New Issue
Block a user