Files
ihaleist/server/dt-store.mjs
T

170 lines
6.8 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.
/**
* 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();
}
}