114 lines
5.3 KiB
JavaScript
114 lines
5.3 KiB
JavaScript
/**
|
||
* ihaleist canlı veri istemcisi.
|
||
* Backend proxy (server/index.js) üzerinden ilan.gov.tr resmi ihale ilanlarını çeker.
|
||
* Dev'de Vite proxy'si /api isteklerini localhost:3001'e yönlendirir.
|
||
*/
|
||
|
||
function sourceError(data, temporaryMessage) {
|
||
if (data.code === 'HUMAN_VERIFICATION_REQUIRED' && data.manualAction) {
|
||
return new Error(data.error || 'EKAP güvenlik doğrulaması gerekiyor. EKAP’ı kendi tarayıcınızda açın.');
|
||
}
|
||
if (data.code === 'SOURCE_UNAVAILABLE' && data.retryable) return new Error(temporaryMessage);
|
||
return null;
|
||
}
|
||
|
||
export async function fetchLiveTenders({ refresh = false } = {}) {
|
||
const res = await fetch(`/api/tenders${refresh ? '?refresh=1' : ''}`);
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
const failure = sourceError(data, 'İhale kaynağı geçici olarak erişilemiyor. Birkaç dakika sonra yeniden deneyin.');
|
||
if (failure) throw failure;
|
||
throw new Error(`API hatası: HTTP ${res.status}`);
|
||
}
|
||
if (!data.success) throw new Error(data.error || 'API başarısız yanıt döndü');
|
||
return data;
|
||
}
|
||
|
||
export async function fetchTenderDetail(tenderId, {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 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});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
const failure = sourceError(data, 'Resmi ihale kaynağı geçici olarak erişilemiyor. Birkaç dakika sonra yeniden deneyin.');
|
||
if (failure) throw failure;
|
||
throw new Error(`Detay API hatası: HTTP ${res.status}`);
|
||
}
|
||
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;
|
||
const warnings = [
|
||
!data.ilanSayisi && 'EKAP ilan metni bulunamadı; yalnız temel ihale bilgileri alındı. Resmi dokümanı kontrol edin.',
|
||
data.sourceDateWarning,
|
||
].filter(Boolean);
|
||
data.sourceWarning=warnings.length ? warnings.join(' ') : null;
|
||
}
|
||
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;
|
||
} catch (error) {
|
||
if (timedOut) throw new Error('İlan metni yüklenirken zaman aşımı oluştu. Yeniden deneyin.');
|
||
throw error;
|
||
} finally {
|
||
clearTimeout(timer); signal?.removeEventListener('abort',cancel);
|
||
}
|
||
}
|
||
|
||
// Live ilan.gov.tr rows can carry an older announcement date than EKAP's
|
||
// authoritative tender detail. Fetch the official record when a user opens a
|
||
// tender so the modal/card can reconcile the date without enriching the full
|
||
// pool (which would be too expensive for thousands of rows).
|
||
export async function fetchOfficialTenderDetail(ikn, {signal, timeoutMs = 30000} = {}) {
|
||
const cleanIkn = String(ikn || '').replace(/[\s\u00a0]+/g, '');
|
||
if (!/^\d{4}\/\d+$/.test(cleanIkn)) throw new Error('Geçersiz İKN');
|
||
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 res = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(cleanIkn)}`, { signal: controller.signal });
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
const failure = sourceError(data, 'Resmî ihale kaynağı geçici olarak erişilemiyor.');
|
||
if (failure) throw failure;
|
||
throw new Error(`Resmî detay API hatası: HTTP ${res.status}`);
|
||
}
|
||
if (!data.success || data.ikn !== cleanIkn) throw new Error('Resmî detay yanıtı aranan İKN ile uyuşmuyor.');
|
||
if (!data.tarih || !data.saat) throw new Error('Resmî detayda ihale tarih ve saati bulunamadı.');
|
||
return data;
|
||
} catch (error) {
|
||
if (timedOut) throw new Error('Resmî ihale tarihi yüklenirken zaman aşımı oluştu.');
|
||
throw error;
|
||
} finally {
|
||
clearTimeout(timer);
|
||
signal?.removeEventListener('abort', cancel);
|
||
}
|
||
}
|
||
|
||
// Kullanıcının ihale üzerindeki durumunu (kanban, takip vb.) canlı veriyle birleştirir.
|
||
const USER_FIELDS = ['kanbanStatus', 'takipte', 'incelendi', 'atananPersonel', 'gorev', 'notes', 'checklist', 'bidDraft'];
|
||
|
||
export function mergeUserState(liveTenders, previousTenders) {
|
||
const prevById = new Map((previousTenders || []).map((t) => [t.id, t]));
|
||
return liveTenders.map((tender) => {
|
||
const prev = prevById.get(tender.id);
|
||
if (!prev) return tender;
|
||
const merged = { ...tender };
|
||
for (const f of USER_FIELDS) {
|
||
if (prev[f] !== undefined) merged[f] = prev[f];
|
||
}
|
||
if (prev.aciklama && !merged.aciklama) merged.aciklama = prev.aciklama;
|
||
return merged;
|
||
});
|
||
}
|