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
+109
View File
@@ -0,0 +1,109 @@
/**
* DT worker'ının doldurduğu SQLite'ı okuyup ihaleist tender modeline çevirir.
* Veritabanı yoksa (worker hiç koşmadıysa) sessizce boş liste döner.
*/
import { DatabaseSync } from 'node:sqlite';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_PATH = path.join(__dirname, 'data', 'dt.sqlite');
function parseDetail(detailJson) {
if (!detailJson) return {};
try {
const d = JSON.parse(detailJson).dogrudanTeminDetayResult || {};
const dt = d.DogrudanTeminBilgileri || {};
const idare = d.IdareBilgileri || {};
const soz = d.SozlesmeBilgileri?.SozlesmeBilgisiList?.[0] || null;
return {
yasaMaddesi: dt.YasaKapsamiTeminMaddesi || null,
durum: dt.DtDurumu || null,
bransKodlari: dt.BransKodList || [],
kismiTeklif: dt.KismiTeklif || null,
idareTam: idare.Idare || null,
enUstIdare: idare.EnUstIdare || null,
kazananFirma: soz?.IstekliAdi || null,
sozlesmeBedeli: soz?.SozlesmeBedeli || null,
sozlesmeTarihi: soz?.SozlesmeTarihi || null,
};
} catch {
return {};
}
}
const KURUM_RULES = [
[/hastane|sağlık|başhekim|tıp merkezi|adsm|ağız ve diş/i, 'Sağlık Kurumları'],
[/ceza infaz|cezaevi|tutukevi|işyurt/i, 'Ceza İnfaz Kurumları'],
[/üniversite|rektörlü/i, 'Üniversiteler'],
[/büyükşehir belediye/i, 'Büyükşehir Belediyeleri'],
[/belediye/i, 'Belediyeler'],
[/bakanlığı|valiliği|müdürlüğü|komutanlığı/i, 'Bakanlık / Kamu Kurumları'],
];
function classifyKurum(text) {
for (const [re, label] of KURUM_RULES) if (re.test(text || '')) return label;
return 'Diğer Kamu Kurumları';
}
function mapDtRow(row) {
const det = parseDetail(row.detail_json);
const kurum = det.idareTam || row.idare_adi || '';
return {
id: `dt-${row.dogrudan_temin_id}`,
kaynak: 'EKAP · Doğrudan Temin',
kaynakUrl: 'https://ekap.kik.gov.tr/EKAP/YeniIhaleArama.aspx',
ikn: row.dtn || '',
baslik: row.isin_adi || '',
kurum,
sehir: row.ili && !row.ili.startsWith('Plaka') ? row.ili : '',
bolge: '',
tarih: row.dt_tarihi || '',
saat: '',
alimTuru: 'Doğrudan Temin',
usul: det.yasaMaddesi ? `Doğrudan Temin (4734 ${det.yasaMaddesi})` : 'Doğrudan Temin (4734 22/d)',
sektor: row.turu || 'Genel',
brans: (det.bransKodlari || []).join(', ') || row.turu || 'Genel',
kurumTipi: classifyKurum(kurum),
eklenmeZamani: row.yayim_tarihi ? `Yayım: ${row.yayim_tarihi}` : '',
isYeni: false,
isLiveFeed: true,
aciliyetBadge: null,
items: [],
aciklama: null,
yaklasikMaliyet: null,
teslimatSuresi: null,
teslimYeri: null,
yeterlikKriterleri: [],
aiMatchScore: null,
aiRiskScore: null,
aiBidRange: null,
kanbanStatus: 'incelenecek',
atananPersonel: null,
durum: det.durum || 'Aktif',
incelendi: false,
takipte: false,
// Sonuçlanmış DT'lerde kazanan/fiyat bilgisi
sozlesmeBedeli: det.sozlesmeBedeli || null,
kazananFirma: det.kazananFirma || null,
};
}
export function readDtTenders(limit = 300) {
if (!fs.existsSync(DB_PATH)) return { tenders: [], totalCount: 0, available: false };
const db = new DatabaseSync(DB_PATH, { readOnly: true });
try {
const total = db.prepare('SELECT COUNT(*) c FROM dt_tenders').get().c;
const rows = db
.prepare(
`SELECT dogrudan_temin_id, dtn, isin_adi, idare_adi, turu, ili, dt_tarihi, yayim_tarihi, detail_json
FROM dt_tenders ORDER BY rowid DESC LIMIT ?`,
)
.all(limit);
return { tenders: rows.map(mapDtRow), totalCount: total, available: true };
} finally {
db.close();
}
}