reconcile official tender dates on detail open
Quality gates / test-build (push) Canceled after 0s
Quality gates / container (push) Canceled after 0s

This commit is contained in:
2026-09-06 20:42:21 +03:00
parent 3042ec58af
commit a268b15ad7
4 changed files with 74 additions and 4 deletions
+21 -3
View File
@@ -8,7 +8,7 @@ import React, {createContext, useContext, useState, useEffect, useMemo, useRef }
import { scanCompanyProposal } from '../services/companyProfile';
import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum';
import { getToken } from '../services/auth';
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
import { fetchLiveTenders, fetchTenderDetail, fetchOfficialTenderDetail, mergeUserState } from '../services/tenderApi';
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
import { getUserDataSnapshot, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
import { applyTenderStates, tenderStateKey, tenderSnapshot, publicTender, tenderSourceChanges } from '../services/tenderState';
@@ -321,9 +321,27 @@ export const AppProvider = ({ children }) => {
const controller = new AbortController();
setDetailLoad({id:t.id,status:'loading'});
fetchTenderDetail(t.id, {signal:controller.signal})
.then(detail => {
.then(async detail => {
if (cancelled) return;
const source = publicTender(detail);
let enriched = detail;
if (t.id.startsWith('ilan-') && /^\d{4}\/\d+$/.test(String(t.ikn || '').replace(/[\s\u00a0]+/g, ''))) {
try {
const official = await fetchOfficialTenderDetail(t.ikn, { signal: controller.signal });
enriched = {
...detail,
...official,
// Keep the richer announcement text if EKAP returned only the
// compact metadata block.
aciklama: String(official.metin || '').length > String(detail.aciklama || '').length
? official.metin : detail.aciklama,
};
} catch (error) {
if (cancelled || controller.signal.aborted) return;
enriched = { ...detail, sourceWarning: `Resmî EKAP tarihi doğrulanamadı; ilan tarihi gösteriliyor. ${error.message}` };
}
}
if (cancelled) return;
const source = publicTender(enriched);
source.sourceChanges = tenderSourceChanges(savedTenderStates.current[tenderStateKey(t)]?.snapshot, {...t,...source});
setDetailLoad({id:t.id,status:'loaded'});
setActiveTenderDetail(prev =>
+32
View File
@@ -52,6 +52,38 @@ export async function fetchTenderDetail(tenderId, {signal, timeoutMs = 30000} =
}
}
// 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) {
if (data.code === 'SOURCE_UNAVAILABLE' && data.retryable) throw new Error('Resmî ihale kaynağı geçici olarak erişilemiyor.');
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'];