170 lines
6.7 KiB
JavaScript
170 lines
6.7 KiB
JavaScript
/**
|
||
* 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 || '',
|
||
saat: (det.tarihSaat || '').split(' ')[1] || '',
|
||
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();
|
||
}
|
||
}
|