feat: canlı veri — ilan.gov.tr açık ihale + EKAP doğrudan temin (22/d)

- server/index.js: ilan.gov.tr proxy (açık ihale + kira/satış), TLS zincir düzeltmesi
- server/dt-worker.mjs: EKAP doğrudan temin toplayıcı (Playwright, imzasız oturum içi fetch)
- server/dt-store.mjs + il-plaka.mjs: DT verisini tender modeline çevirme
- src/services/tenderApi.js + AppContext: mock veri yerine canlı API
- Doğrudan Temin sekmesi 256 gerçek 22/d kaydıyla dolu
This commit is contained in:
2026-08-26 20:45:02 +03:00
parent d9570fe422
commit 89cfd03d92
14 changed files with 1000 additions and 66 deletions
+44 -53
View File
@@ -1,6 +1,7 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { MOCK_TENDERS, MOCK_LIVE_STATS, MOCK_COMPANY_STATS } from '../data/mockTenders';
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
const AppContext = createContext();
@@ -30,68 +31,56 @@ export const AppProvider = ({ children }) => {
const [isLiveMode, setIsLiveMode] = useState(true);
const [isRefreshingLive, setIsRefreshingLive] = useState(false);
const [liveDataError, setLiveDataError] = useState(null);
const [liveDataSource, setLiveDataSource] = useState(null);
const refreshLiveEKAPData = () => {
// Canlı ihale verisini (ilan.gov.tr) çeker; kullanıcının kanban/takip durumunu korur.
const refreshLiveEKAPData = async ({ refresh = true } = {}) => {
setIsRefreshingLive(true);
setTimeout(() => {
const liveItems = [
{
id: `live-ekap-${Date.now()}`,
ikn: `2026/${Math.floor(100000 + Math.random() * 900000)}`,
baslik: "T.C. SAĞLIK BAKANLIĞI ANKARA ŞEHİR HASTANESİ 18 KALEM TIBBİ SARF MALZEME ALIMI",
kurum: "T.C. SAĞLIK BAKANLIĞI ANKARA ŞEHİR HASTANESİ BAŞHEKİMLİĞİ",
sehir: "Ankara",
bolge: "İÇ ANADOLU BÖLGESİ",
tarih: "14.08.2026",
saat: "10:30",
alimTuru: "Açık İhale",
usul: "Açık İhale Usulü (4734 Sayılı Kanun Madde 19)",
sektor: "Tıbbi Cihaz ve Sarf Malzemeleri",
brans: "Ortopedi & Beyin Cerrahi Sarf Malzemeleri",
kurumTipi: "Eğitim ve Araştırma Hastaneleri",
eklenmeZamani: " Canlı Akış (Canlı Yayınlandı)",
isYeni: true,
isLiveFeed: true,
aciliyetBadge: "🔴 CANLI İHALE",
items: [
{ id: "li1", ad: "Titanyum Pedikül Vidası", miktar: "150", birim: "adet", birimFiyat: 1420 },
{ id: "li2", ad: "Cerrahi Rod (5.5mm)", miktar: "80", birim: "adet", birimFiyat: 680 },
{ id: "li3", ad: "Posterior Servikal Kafes", miktar: "40", birim: "adet", birimFiyat: 2450 }
],
aciklama: "T.C. Sağlık Bakanlığı Ankara Şehir Hastanesi Beyin ve Sinir Cerrahisi Kliniği İhtiyacı İçin Tıbbi Sarf Malzeme Canlı Veri Akışıyla Alınacaktır.",
yaklasikMaliyet: 542000,
teslimatSuresi: "15 Takvim Günü",
teslimYeri: "Ankara Bilkent Şehir Hastanesi Medikal Depo",
yeterlikKriterleri: ["ÜTS Barkod Kaydı", "ISO 13485 Kalite Belgesi", "TSE HYB Satış Yetkisi"],
aiMatchScore: 98,
aiRiskScore: "Düşük",
aiBidRange: { min: 435000, max: 510000, recommended: 468000 },
kanbanStatus: "incelenecek",
atananPersonel: "Hikmet Tanrıverdi",
durum: "Aktif",
incelendi: false,
takipte: true,
sozlesmeBedeli: null,
kazananFirma: null
}
];
setTenders(prev => {
const filtered = prev.filter(t => !t.id.startsWith('live-ekap-'));
return [...liveItems, ...filtered];
});
setLiveDataError(null);
try {
const data = await fetchLiveTenders({ refresh });
setTenders(prev => mergeUserState(data.tenders, prev));
setLiveDataSource({ source: data.source, fetchedAt: data.fetchedAt });
setLiveStats(prev => ({
...prev,
todayAdded: prev.todayAdded + 1,
totalItems: prev.totalItems + 1
bugunEklenen: data.todayCount ?? prev.bugunEklenen,
aktifTakip: data.totalCount ?? prev.aktifTakip
}));
} catch (err) {
setLiveDataError(err.message);
} finally {
setIsRefreshingLive(false);
}, 1000);
}
};
// Açılışta canlı veriyi yükle (başarısız olursa mevcut/mock veri ekranda kalır)
useEffect(() => {
refreshLiveEKAPData({ refresh: false });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [activeTenderDetail, setActiveTenderDetail] = useState(null);
// Canlı ihalede detay açıldığında ilan metnini API'den tamamla
useEffect(() => {
const t = activeTenderDetail;
if (!t || !t.id?.startsWith('ilan-') || t.aciklama) return;
let cancelled = false;
fetchTenderDetail(t.id)
.then(detail => {
if (cancelled || !detail.aciklama) return;
setActiveTenderDetail(prev =>
prev && prev.id === t.id ? { ...prev, aciklama: detail.aciklama } : prev
);
setTenders(prev =>
prev.map(x => (x.id === t.id ? { ...x, aciklama: detail.aciklama } : x))
);
})
.catch(() => {});
return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTenderDetail?.id]);
const [isDetailedSearchOpen, setIsDetailedSearchOpen] = useState(false);
const [isCompanyScannerOpen, setIsCompanyScannerOpen] = useState(false);
const [isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen] = useState(false);
@@ -317,6 +306,8 @@ export const AppProvider = ({ children }) => {
setIsLiveMode,
isRefreshingLive,
refreshLiveEKAPData,
liveDataError,
liveDataSource,
liveStats,
searchQuery,
setSearchQuery,