/** * 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 fs from 'node:fs'; import { sektorKarar } from './siniflandir.mjs'; import { classifyCpvByKeyword } from './cpv-classify.mjs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { titleCaseTr } from './tr-text.mjs'; // node:sqlite bazı Node sürümlerinde deneysel/flag'li. Yoksa DT devre dışı kalır, // ilan.gov.tr canlı verisi yine de çalışır. let DatabaseSync = null; try { ({ DatabaseSync } = await import('node:sqlite')); } catch { console.warn('[dt-store] node:sqlite yok — doğrudan temin devre dışı (açık ihaleler etkilenmez)'); } 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; const ilan = d.IlanBilgileri || {}; const duyuru = ilan.DogrudanTeminIlanBilgisiList?.[0] || null; const sonuc = ilan.SonucIlanBilgisiList?.[0] || null; const iptal = ilan.IptalIlanBilgisiList?.[0] || null; return { yasaMaddesi: dt.YasaKapsamiTeminMaddesi || null, istisnaDayanagi: dt.IstisnaAliminDayanagi || null, durum: dt.DtDurumu || null, // BransKodList aslında OKAS (CPV) kodudur — idarenin kendi sınıflandırması. bransKodlari: dt.BransKodList || [], kismiTeklif: dt.KismiTeklif || null, kisimSayisi: dt.KisimSayisi || null, ilaninSekli: dt.IlaninSekli || null, tarihSaat: dt.DtTarihSaati || null, eIhale: Boolean(dt.EIhale), duyuruYapilacak: Boolean(dt.DogrudanTeminDuyurusuYapilacakMi), sozlesmeTasarisiVar: Boolean(dt.DogrudanTeminSozlesmeTasarisiVarMi), iptalNedeni: dt.IptalNedeni || null, iptalTarihi: dt.IptalTarihi || null, idareTam: idare.Idare || null, ustIdare: idare.UstIdare || null, enUstIdare: idare.EnUstIdare || null, ili: idare.Ili || null, // İlan belgeleri — EncIlanId ile EKAP'ta görüntülenir // EncIlanId → EKAP ilan görüntüleme adresi. Doğrulanmış tek yol // EKAP arayüzü olduğundan bağlantıyı kullanıcının tarayıcısında açarız. ilanlar: [ duyuru && { tip: 'Doğrudan Temin Duyurusu', tarih: duyuru.IlanTarihi, encId: duyuru.EncIlanId }, sonuc && { tip: 'Sonuç İlanı', tarih: sonuc.IlanTarihi, encId: sonuc.EncIlanId }, iptal && { tip: 'İptal İlanı', tarih: iptal.IlanTarihi, encId: iptal.EncIlanId }, ].filter(Boolean), 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 || ''; const tender = { 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') ? titleCaseTr(row.ili) : '', bolge: '', tarih: (row.dt_tarihi || '').split(' ')[0], saat: (det.tarihSaat || row.dt_tarihi || '').match(/\d{2}:\d{2}/)?.[0] || '', alimTuru: 'Doğrudan Temin', usul: det.yasaMaddesi ? `Doğrudan Temin (4734 ${det.yasaMaddesi})` : det.istisnaDayanagi ? `Doğrudan Temin (istisna ${det.istisnaDayanagi})` : 'Doğrudan Temin', // BransKodList = OKAS kodu → idarenin kendi sınıflandırması, başlıktan // güvenilir. Kod yoksa iş adına düşülür. sektor: sektorKarar( (det.bransKodlari || []).map((k) => ({ kod: k })), row.isin_adi || '', ), brans: (det.bransKodlari || []).join(', ') || row.turu || 'Genel', kurumTipi: classifyKurum(kurum), eklenmeZamani: row.yayim_tarihi ? `Yayım: ${row.yayim_tarihi}` : '', ilanTarihi: row.yayim_tarihi || '', isYeni: false, isLiveFeed: true, aciliyetBadge: null, // OKAS kalemleri — arayüzde kod+ad gösterilebilsin items: (det.bransKodlari || []).map((k, i) => ({ id: `okas-${i}`, kod: k, ad: `OKAS ${k}`, miktar: null, birim: null, birimFiyat: null, })), ilanBelgeleri: det.ilanlar || [], ilaninSekli: det.ilaninSekli || null, kismiTeklif: det.kismiTeklif || null, eIhale: det.eIhale || false, iptalNedeni: det.iptalNedeni || null, 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, }; // CPV/OKAS — YAYINLANMADAN ÖNCE tamamlanır (bkz. cpv-classify.mjs). // BransKodList zaten gerçek OKAS kodu taşır; 'existing' dalı bunu anında // kullanır, AI'a nadiren düşer. const cpv = classifyCpvByKeyword(tender) || { cpv: [], cpvBolum: [], cpvLabel: 'Genel', cpvSource: 'none' }; tender.cpv = cpv.cpv; tender.cpvBolum = cpv.cpvBolum; tender.cpvLabel = cpv.cpvLabel; tender.cpvSource = cpv.cpvSource; return tender; } export function readDtTenders(limit = 300) { if (!DatabaseSync || !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(); } }