511 lines
19 KiB
JavaScript
511 lines
19 KiB
JavaScript
/**
|
||
* 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 { sektorKarar } from './siniflandir.mjs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { titleCaseTr } from './tr-text.mjs';
|
||
|
||
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 sektor_zengin (
|
||
ilan_id TEXT PRIMARY KEY, -- ör. ilan-12345
|
||
sektor TEXT NOT NULL, -- doküman metninden çıkarılan sektör
|
||
kaynak TEXT, -- 'dokuman' | 'okas'
|
||
zaman TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS havuz_olcum (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
zaman TEXT NOT NULL,
|
||
kaynak_toplam INTEGER, -- ilan.gov.tr'nin bildirdiği numFound
|
||
yuklenen INTEGER, -- bizim havuzumuza giren kayıt
|
||
eksik INTEGER, -- kaynak_toplam - yuklenen
|
||
dt INTEGER, -- doğrudan temin kaydı
|
||
arsiv INTEGER -- geçmiş sonuç arşivi
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_havuz_zaman ON havuz_olcum(zaman);
|
||
|
||
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ş: sektör zenginleştirmesi ilanın TAMAMINI tarıyordu ve standart
|
||
// hukuki metin ("e-imza", "numune sunulması") yanlış sınıflandırma üretiyordu.
|
||
// v2 yalnızca işin konusunu tarar; eski kayıtlar silinip yeniden hesaplanır.
|
||
try {
|
||
const silinen = db.prepare("DELETE FROM sektor_zengin WHERE kaynak IS NULL OR kaynak <> 'dokuman-v2'").run();
|
||
if (silinen?.changes) console.log(`[history-store] ${silinen.changes} eski sektör kaydı geçersiz kılındı (v2)`);
|
||
} catch { /* tablo henüz yoksa sorun değil */ }
|
||
|
||
// 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: titleCaseTr(r.il),
|
||
tarih: r.ihale_tarihi || '',
|
||
alimTuru: r.ihale_tipi || 'İhale',
|
||
// Sektör ihale TÜRÜ değildir ('Mal Alımı' bir sektör değil).
|
||
// Önce OKAS kodu (idarenin kendi sınıflandırması), yoksa ihale adı.
|
||
sektor: sektorKarar(okasParse(r.okas_kalemler, null), r.ihale_adi || ''),
|
||
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,
|
||
};
|
||
}
|
||
|
||
// OKAS kalemlerinden ad listesi — sektör tahminini güçlendirir.
|
||
function kalemAdlari(ham) {
|
||
if (!ham) return '';
|
||
try {
|
||
const k = JSON.parse(ham);
|
||
return Array.isArray(k) ? k.map((x) => x?.ad || '').join(' ') : '';
|
||
} catch {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
|
||
// --- Havuz izleme ----------------------------------------------------------
|
||
// Havuz büyüklüğü kaynağa (ilan.gov.tr) bağlı ve gün içinde ciddi dalgalanıyor.
|
||
// Ölçümleri saklarız ki "veri mi kayboldu, kaynak mı düştü" sorusu bir daha
|
||
// tahmine kalmasın.
|
||
|
||
/** Bir ölçüm kaydeder ve önceki ölçüme göre ani düşüş olup olmadığını döndürür. */
|
||
export function havuzOlc({ kaynakToplam, yuklenen, dt = null, arsiv = null }) {
|
||
const db = openDb({ create: true });
|
||
if (!db) return null;
|
||
try {
|
||
const onceki = db
|
||
.prepare('SELECT zaman, kaynak_toplam, yuklenen FROM havuz_olcum ORDER BY id DESC LIMIT 1')
|
||
.get() || null;
|
||
|
||
// 7 günlük referans — tek bir ölçüme değil, ortalamaya kıyaslarız ki
|
||
// havuz yavaş yavaş erirse de yakalayalım.
|
||
const ortalamaAl = (gun) => {
|
||
const bas = new Date(Date.now() - gun * 864e5).toISOString();
|
||
return db
|
||
.prepare(`SELECT AVG(kaynak_toplam) ort, COUNT(kaynak_toplam) adet
|
||
FROM havuz_olcum WHERE zaman >= ? AND kaynak_toplam IS NOT NULL`)
|
||
.get(bas) || {};
|
||
};
|
||
// 7 gün: günlük dalgalanma. 30 gün: kalıcı erime — 7 günlük ortalama
|
||
// düşüşe ayak uydurup alarmı susturduğunda bunu ancak 30 gün yakalar.
|
||
const ref = ortalamaAl(7);
|
||
const ref30 = ortalamaAl(30);
|
||
|
||
db.prepare(
|
||
`INSERT INTO havuz_olcum (zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv)
|
||
VALUES (?,?,?,?,?,?)`
|
||
).run(
|
||
new Date().toISOString(),
|
||
kaynakToplam ?? null,
|
||
yuklenen ?? null,
|
||
kaynakToplam != null && yuklenen != null ? kaynakToplam - yuklenen : null,
|
||
dt, arsiv
|
||
);
|
||
|
||
// 90 günden eski ölçümleri at — bu tablo sınırsız büyümemeli.
|
||
const sinir = new Date(Date.now() - 90 * 864e5).toISOString();
|
||
db.prepare('DELETE FROM havuz_olcum WHERE zaman < ?').run(sinir);
|
||
|
||
return degerlendir(onceki, ref, ref30, { kaynakToplam, yuklenen });
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
// Ortalamanın anlamlı olması için gereken en az ölçüm sayısı.
|
||
const MIN_OLCUM = 5;
|
||
// Bu oranın altına düşerse uyarılır (7 günlük ortalamanın %60'ı).
|
||
const DUSUS_ESIGI = 0.6;
|
||
// 30 günlük referans için: daha çok ölçüm ister, eşiği daha toleranslıdır —
|
||
// amaç ani dalgalanma değil, kalıcı erimeyi görmek.
|
||
const MIN_OLCUM_30 = 20;
|
||
const DUSUS_ESIGI_30 = 0.7;
|
||
|
||
/**
|
||
* Ölçümü 7 GÜNLÜK ORTALAMAYA kıyaslar; yeterli geçmiş yoksa bir önceki
|
||
* ölçüme düşer. Uyarı gerekiyorsa açıklamasını döndürür.
|
||
*/
|
||
function degerlendir(onceki, ref, ref30, { kaynakToplam, yuklenen }) {
|
||
const ortalama = ref?.adet >= MIN_OLCUM && ref.ort ? Math.round(ref.ort) : null;
|
||
const ortalama30 = ref30?.adet >= MIN_OLCUM_30 && ref30.ort ? Math.round(ref30.ort) : null;
|
||
const sonuc = {
|
||
uyari: null,
|
||
onceki: onceki?.kaynak_toplam ?? null,
|
||
simdi: kaynakToplam ?? null,
|
||
haftalikOrtalama: ortalama,
|
||
aylikOrtalama: ortalama30,
|
||
olcumAdedi: ref?.adet ?? 0,
|
||
olcumAdedi30: ref30?.adet ?? 0,
|
||
};
|
||
|
||
// 1) Kaynak düştü mü? (bizim hatamız değil ama bilmek isteriz)
|
||
if (kaynakToplam != null) {
|
||
if (ortalama > 0) {
|
||
const oran = kaynakToplam / ortalama;
|
||
sonuc.ortalamayaOran = Number(oran.toFixed(3));
|
||
if (oran < DUSUS_ESIGI) {
|
||
sonuc.uyari = `Kaynak havuzu 7 günlük ortalamanın %${Math.round((1 - oran) * 100)} altında: ` +
|
||
`ortalama ${ortalama}, şimdi ${kaynakToplam} ilan (ilan.gov.tr tarafı).`;
|
||
sonuc.tur = 'kaynak_dusus';
|
||
}
|
||
} else if (onceki?.kaynak_toplam > 0) {
|
||
// Henüz ortalama için yeterli ölçüm yok — geçici olarak son ölçüme bakarız.
|
||
const oran = kaynakToplam / onceki.kaynak_toplam;
|
||
if (oran < 0.5) {
|
||
sonuc.uyari = `Kaynak havuzu %${Math.round((1 - oran) * 100)} düştü: ` +
|
||
`${onceki.kaynak_toplam} → ${kaynakToplam} ilan (ilan.gov.tr tarafı). ` +
|
||
`[ortalama için henüz ${ref?.adet ?? 0}/${MIN_OLCUM} ölçüm var]`;
|
||
sonuc.tur = 'kaynak_dusus';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 1b) Kalıcı erime: 7 günlük ortalama düşüşe ayak uydurduğu için sustuysa
|
||
// bile, 30 günlük referans hâlâ çok yukarıdaysa bunu ayrıca bildiririz.
|
||
if (kaynakToplam != null && ortalama30 > 0) {
|
||
const oran30 = kaynakToplam / ortalama30;
|
||
sonuc.aylikOrtalamayaOran = Number(oran30.toFixed(3));
|
||
if (oran30 < DUSUS_ESIGI_30 && !sonuc.uyari) {
|
||
sonuc.uyari = `Kaynak havuzu 30 günlük ortalamanın %${Math.round((1 - oran30) * 100)} altında ` +
|
||
`(kalıcı düşüş): ortalama ${ortalama30}, şimdi ${kaynakToplam} ilan.`;
|
||
sonuc.tur = 'kalici_dusus';
|
||
} else if (oran30 < DUSUS_ESIGI_30 && sonuc.uyari) {
|
||
// Zaten haftalık uyarı var; 30 günlük bağlamı mesaja ekle.
|
||
sonuc.uyari += ` (30 günlük ortalama ${ortalama30}, oran %${Math.round(oran30 * 100)})`;
|
||
}
|
||
}
|
||
|
||
// 2) Biz kaynağın gerisinde mi kaldık? (bu BİZİM hatamız olur)
|
||
if (!sonuc.uyari && kaynakToplam > 0 && yuklenen != null && yuklenen < kaynakToplam * 0.9) {
|
||
sonuc.uyari = `Havuz eksik yüklendi: kaynakta ${kaynakToplam}, bizde ${yuklenen} ilan.`;
|
||
sonuc.tur = 'eksik_yukleme';
|
||
}
|
||
|
||
return sonuc;
|
||
}
|
||
|
||
/** Son ölçümler — /api/health/havuz ve arayüz için. */
|
||
export function havuzGecmisi(limit = 60) {
|
||
const db = openDb();
|
||
if (!db) return [];
|
||
try {
|
||
return db
|
||
.prepare('SELECT zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv FROM havuz_olcum ORDER BY id DESC LIMIT ?')
|
||
.all(Math.min(Number(limit) || 60, 500));
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Havuz özeti — 7 ve 30 günlük ortalamalar SQL'de hesaplanır; döndürülen
|
||
* satır sayısından bağımsızdır (60 kayıtlık pencereye sıkışmaz).
|
||
*/
|
||
export function havuzOzet() {
|
||
const db = openDb();
|
||
if (!db) return null;
|
||
try {
|
||
const pencere = (gun) => {
|
||
const bas = new Date(Date.now() - gun * 864e5).toISOString();
|
||
const r = db
|
||
.prepare(`SELECT AVG(kaynak_toplam) ort, MAX(kaynak_toplam) zirve, MIN(kaynak_toplam) dip,
|
||
COUNT(kaynak_toplam) adet
|
||
FROM havuz_olcum WHERE zaman >= ? AND kaynak_toplam IS NOT NULL`)
|
||
.get(bas) || {};
|
||
return {
|
||
ortalama: r.ort ? Math.round(r.ort) : null,
|
||
zirve: r.zirve ?? null,
|
||
dip: r.dip ?? null,
|
||
adet: r.adet ?? 0,
|
||
};
|
||
};
|
||
const son = db
|
||
.prepare('SELECT zaman, kaynak_toplam, yuklenen, eksik, dt, arsiv FROM havuz_olcum ORDER BY id DESC LIMIT 1')
|
||
.get() || null;
|
||
return { son, hafta: pencere(7), ay: pencere(30) };
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
|
||
// --- Sektör zenginleştirme (doküman metninden) -----------------------------
|
||
// Başlığı sinyalsiz ihaleler ("MUHTELİF MALZEME ALIMI") ilan metninden
|
||
// sınıflandırılır. Sonuç kalıcı saklanır: aynı ilan bir daha indirilmez.
|
||
|
||
/** id → sektör haritası. Yalnızca istenen id'ler okunur. */
|
||
export function sektorZenginOku(idler) {
|
||
const db = openDb();
|
||
if (!db || !idler?.length) return new Map();
|
||
try {
|
||
const harita = new Map();
|
||
// SQLite parametre sınırına takılmamak için parçalara böl.
|
||
for (let i = 0; i < idler.length; i += 400) {
|
||
const parca = idler.slice(i, i + 400);
|
||
const yer = parca.map(() => '?').join(',');
|
||
const satirlar = db
|
||
.prepare(`SELECT ilan_id, sektor FROM sektor_zengin WHERE ilan_id IN (${yer})`)
|
||
.all(...parca);
|
||
for (const r of satirlar) harita.set(r.ilan_id, r.sektor);
|
||
}
|
||
return harita;
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
export function sektorZenginYaz(kayitlar) {
|
||
const db = openDb({ create: true });
|
||
if (!db || !kayitlar?.length) return 0;
|
||
try {
|
||
const st = db.prepare(
|
||
`INSERT INTO sektor_zengin (ilan_id, sektor, kaynak, zaman) VALUES (?,?,?,?)
|
||
ON CONFLICT(ilan_id) DO UPDATE SET sektor=excluded.sektor, kaynak=excluded.kaynak, zaman=excluded.zaman`
|
||
);
|
||
const now = new Date().toISOString();
|
||
let n = 0;
|
||
for (const k of kayitlar) {
|
||
if (!k?.id || !k?.sektor) continue;
|
||
st.run(k.id, k.sektor, k.kaynak || 'dokuman', now);
|
||
n++;
|
||
}
|
||
return n;
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
/** Hangi id'ler daha önce denenmiş? ('Genel' kaydı da denenmiş sayılır) */
|
||
export function sektorZenginDenenmis(idler) {
|
||
const db = openDb();
|
||
if (!db || !idler?.length) return new Set();
|
||
try {
|
||
const küme = new Set();
|
||
for (let i = 0; i < idler.length; i += 400) {
|
||
const parca = idler.slice(i, i + 400);
|
||
const yer = parca.map(() => '?').join(',');
|
||
for (const r of db.prepare(`SELECT ilan_id FROM sektor_zengin WHERE ilan_id IN (${yer})`).all(...parca))
|
||
küme.add(r.ilan_id);
|
||
}
|
||
return küme;
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
export function sektorZenginSayi() {
|
||
const db = openDb();
|
||
if (!db) return { toplam: 0, cozulen: 0 };
|
||
try {
|
||
const t = db.prepare("SELECT COUNT(*) c FROM sektor_zengin").get().c;
|
||
const c = db.prepare("SELECT COUNT(*) c FROM sektor_zengin WHERE sektor <> 'Genel'").get().c;
|
||
return { toplam: t, cozulen: c };
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|