/** * Geçmiş ihale sonuçları deposu (SQLite). * ekap-scan.mjs doldurur; server/index.js okur. * * Kalıcı anahtar: IKN (EKAP'ın döndürdüğü `id` per-response token'dır, saklanmaz). */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; let DatabaseSync = null; try { ({ DatabaseSync } = await import('node:sqlite')); } catch { console.warn('[history-store] node:sqlite yok — geçmiş veri deposu devre dışı'); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DATA_DIR = path.join(__dirname, 'data'); const DB_PATH = path.join(DATA_DIR, 'history.sqlite'); const SCHEMA = ` CREATE TABLE IF NOT EXISTS sonuclar ( ikn TEXT NOT NULL, kisim INTEGER NOT NULL DEFAULT 0, ihale_adi TEXT, idare_adi TEXT, il TEXT, ihale_tipi TEXT, ihale_tarihi TEXT, ilan_tarihi TEXT, kazanan_firma TEXT, kazanan_norm TEXT, kazanan_adres TEXT, sozlesme_bedeli REAL, para_birimi TEXT, sozlesme_tarihi TEXT, yaklasik_maliyet REAL, teklif_sayisi INTEGER, gecerli_teklif_sayisi INTEGER, okas_kalemler TEXT, cekilme_zamani TEXT, PRIMARY KEY (ikn, kisim) ); CREATE INDEX IF NOT EXISTS idx_kazanan ON sonuclar(kazanan_norm); CREATE INDEX IF NOT EXISTS idx_idare ON sonuclar(idare_adi); CREATE INDEX IF NOT EXISTS idx_il ON sonuclar(il); CREATE INDEX IF NOT EXISTS idx_tarih ON sonuclar(ihale_tarihi); CREATE TABLE IF NOT EXISTS tarama_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, baslangic TEXT, bitis TEXT, aralik_bas TEXT, aralik_son TEXT, taranan INTEGER, eklenen INTEGER, hata INTEGER, not_ TEXT ); `; export function available() { return Boolean(DatabaseSync); } export function openDb({ create = false } = {}) { if (!DatabaseSync) return null; if (!fs.existsSync(DB_PATH)) { if (!create) return null; fs.mkdirSync(DATA_DIR, { recursive: true }); } const db = new DatabaseSync(DB_PATH); db.exec(SCHEMA); // Geçiş: eski şemada okas_kalemler yoksa ekle try { const kolonlar = db.prepare('PRAGMA table_info(sonuclar)').all().map((c) => c.name); if (!kolonlar.includes('okas_kalemler')) { db.exec('ALTER TABLE sonuclar ADD COLUMN okas_kalemler TEXT'); console.log('[history-store] okas_kalemler kolonu eklendi'); } } catch (e) { console.warn('[history-store] şema geçişi atlandı:', e.message); } return db; } export function saveResults(db, tender, results) { const stmt = db.prepare(` INSERT INTO sonuclar (ikn,kisim,ihale_adi,idare_adi,il,ihale_tipi,ihale_tarihi,ilan_tarihi, kazanan_firma,kazanan_norm,kazanan_adres,sozlesme_bedeli,para_birimi,sozlesme_tarihi, yaklasik_maliyet,teklif_sayisi,gecerli_teklif_sayisi,okas_kalemler,cekilme_zamani) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(ikn,kisim) DO UPDATE SET kazanan_firma=excluded.kazanan_firma, kazanan_norm=excluded.kazanan_norm, sozlesme_bedeli=excluded.sozlesme_bedeli, yaklasik_maliyet=excluded.yaklasik_maliyet, teklif_sayisi=excluded.teklif_sayisi, gecerli_teklif_sayisi=excluded.gecerli_teklif_sayisi, okas_kalemler=COALESCE(excluded.okas_kalemler, sonuclar.okas_kalemler), cekilme_zamani=excluded.cekilme_zamani `); const now = new Date().toISOString(); let n = 0; for (const r of results) { stmt.run( r.ikn || tender.ikn, r.kisimIndex ?? 0, tender.ihaleAdi || null, tender.idareAdi || null, tender.ihaleIlAdi || null, tender.ihaleTipAciklama || null, tender.ihaleTarihSaat || null, r.ilanTarihi || null, r.kazananFirma || null, r.kazananNorm || null, r.kazananAdres || null, r.sozlesmeBedeli ?? null, r.paraBirimi || null, r.sozlesmeTarihi || null, r.yaklasikMaliyet ?? null, r.teklifSayisi ?? null, r.gecerliTeklifSayisi ?? null, tender.okasJson ?? null, now ); n++; } return n; } export function hasIkn(db, ikn) { const row = db.prepare('SELECT 1 FROM sonuclar WHERE ikn=? LIMIT 1').get(ikn); return Boolean(row); } // --- Okuma tarafı (API için) -------------------------------------------- // OKAS kalem listesi JSON'unu tender.items biçimine çevirir. // NOT: teklif birim fiyatları kamuya açık DEĞİL. Burada üretilen 'birimFiyat', // sözleşme bedelinin kalem sayısına bölünmesiyle elde edilen YAKLAŞIK bir // göstergedir; kesin birim fiyat değildir. function okasParse(json, bedel) { if (!json) return []; try { const list = JSON.parse(json); if (!Array.isArray(list) || !list.length) return []; const pay = bedel != null && list.length ? Number(bedel) / list.length : null; return list.map((k, i) => ({ id: `okas-${i}`, kod: k.kodu || null, ad: k.adi || k.koduAdi || 'Kalem', miktar: null, birim: null, birimFiyat: pay, yaklasik: true, })); } catch { return []; } } function rowToTender(r) { return { id: `hist-${r.ikn}-${r.kisim}`, kaynak: 'EKAP Sonuç İlanı', ikn: r.ikn, baslik: r.ihale_adi || 'İhale', kurum: r.idare_adi || '—', sehir: r.il || '', tarih: r.ihale_tarihi || '', alimTuru: r.ihale_tipi || 'İhale', sektor: r.ihale_tipi || 'Genel', kazananFirma: r.kazanan_firma, sozlesmeBedeli: r.sozlesme_bedeli, paraBirimi: r.para_birimi, sozlesmeTarihi: r.sozlesme_tarihi, yaklasikMaliyet: r.yaklasik_maliyet, teklifSayisi: r.teklif_sayisi, gecerliTeklifSayisi: r.gecerli_teklif_sayisi, durum: 'Sonuçlandı', items: okasParse(r.okas_kalemler, r.sozlesme_bedeli), // Arayüzün beklediği alanlar (geçmiş kayıtta karşılığı yok — güvenli varsayılan) eklenmeZamani: 'arşivden', isYeni: false, aciliyetBadge: null, usul: r.ihale_tipi || 'İhale', kurumTipi: '', brans: '', bolge: '', aciklama: '', yeterlikKriterleri: [], kanbanStatus: null, atananPersonel: null, takipte: false, incelendi: false, }; } export function readHistory(limit = 500, { firma, idare, il } = {}) { const db = openDb(); if (!db) return { tenders: [], totalCount: 0, available: false }; try { const where = []; const args = []; if (firma) { where.push('kazanan_norm LIKE ?'); args.push(`%${firma}%`); } if (idare) { where.push('idare_adi LIKE ?'); args.push(`%${idare}%`); } if (il) { where.push('il LIKE ?'); args.push(`%${il}%`); } const w = where.length ? `WHERE ${where.join(' AND ')}` : ''; const total = db.prepare(`SELECT COUNT(*) c FROM sonuclar ${w}`).all(...args)[0]?.c ?? 0; const rows = db.prepare(`SELECT * FROM sonuclar ${w} ORDER BY cekilme_zamani DESC LIMIT ?`).all(...args, limit); return { tenders: rows.map(rowToTender), totalCount: total, available: true }; } catch (e) { console.warn('[history-store] okuma hatası:', e.message); return { tenders: [], totalCount: 0, available: false }; } finally { db.close(); } } export function historyStats() { const db = openDb(); if (!db) return { available: false }; try { const c = (q) => db.prepare(q).all()[0] || {}; const toplam = c('SELECT COUNT(*) v FROM sonuclar').v ?? 0; const firma = c('SELECT COUNT(DISTINCT kazanan_norm) v FROM sonuclar WHERE kazanan_norm IS NOT NULL').v ?? 0; const idare = c('SELECT COUNT(DISTINCT idare_adi) v FROM sonuclar').v ?? 0; const hacim = c('SELECT SUM(sozlesme_bedeli) v FROM sonuclar').v ?? 0; const tekTeklif = c('SELECT COUNT(*) v FROM sonuclar WHERE gecerli_teklif_sayisi=1').v ?? 0; const sonTarama = db.prepare('SELECT * FROM tarama_log ORDER BY id DESC LIMIT 1').all()[0] || null; return { available: true, toplam, firma, idare, hacim, tekTeklif, sonTarama }; } catch { return { available: false }; } finally { db.close(); } } export function logScan(db, row) { db.prepare(`INSERT INTO tarama_log (baslangic,bitis,aralik_bas,aralik_son,taranan,eklenen,hata,not_) VALUES (?,?,?,?,?,?,?,?)`) .run(row.baslangic, row.bitis, row.aralikBas, row.aralikSon, row.taranan, row.eklenen, row.hata, row.not || null); }