Files
ihaleist/src/services/tenderApi.js
T

114 lines
5.3 KiB
JavaScript
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.
/**
* 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;
});
}