/** * Ortak kalem havuzu (SQLite). * * NEDEN: Teklif cetveli kalemleri EKAP'ın API'sinde yok, yalnızca ihale * dokümanının içinde — ve doküman sayfası CAPTCHA ile korunuyor, sunucudan * otomatik indirilemiyor. Kullanıcı dokümanı kendi tarayıcısından indirip * yükleyince kalemler buraya yazılıyor ve o ihale HERKESİN kartında * kalemleriyle görünür oluyor. İlk yükleyen herkes için açıyor. * * Kalıcı anahtar İKN — EKAP'ın `id` alanı anlık token, saklanmaz. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { normalizeTr } from './siniflandir.mjs'; let DatabaseSync = null; try { ({ DatabaseSync } = await import('node:sqlite')); } catch { console.warn('[kalem-store] node:sqlite yok — kalem havuzu devre dışı'); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DATA_DIR = process.env.IHALEIST_SHARED_DATA_DIR || path.join(__dirname, 'data'); const DB_PATH = path.join(DATA_DIR, 'kalem.sqlite'); const SCHEMA = ` CREATE TABLE IF NOT EXISTS ihale_kalem ( id INTEGER PRIMARY KEY AUTOINCREMENT, ikn TEXT NOT NULL, sira INTEGER, ad TEXT NOT NULL, ad_norm TEXT NOT NULL, miktar REAL, birim TEXT ); CREATE INDEX IF NOT EXISTS idx_kalem_ikn ON ihale_kalem(ikn); CREATE INDEX IF NOT EXISTS idx_kalem_norm ON ihale_kalem(ad_norm); CREATE TABLE IF NOT EXISTS kalem_kaynak ( ikn TEXT PRIMARY KEY, kalem_sayisi INTEGER NOT NULL DEFAULT 0, dosya TEXT, yontem TEXT, ekleyen TEXT, zaman TEXT ); `; let db = null; export function available() { return !!DatabaseSync; } function open() { if (db) return db; if (!DatabaseSync) throw new Error('Kalem deposu bu sunucuda kullanılamıyor.'); fs.mkdirSync(DATA_DIR, { recursive: true }); const candidate = new DatabaseSync(DB_PATH); try { candidate.exec('PRAGMA journal_mode = WAL;'); candidate.exec(SCHEMA); db = candidate; } catch (error) { candidate.close(); throw error; } return db; } /** * Bir ihalenin kalemlerini yazar (varsa eskisini değiştirir). * Boş liste yazılmaz — hatalı bir ayrıştırma mevcut iyi veriyi silmesin. */ export function kalemYaz(ikn, kalemler, { dosya, yontem, ekleyen } = {}) { const d = open(); if (!d || !ikn || !Array.isArray(kalemler) || kalemler.length === 0) return 0; const sil = d.prepare('DELETE FROM ihale_kalem WHERE ikn = ?'); const ekle = d.prepare( 'INSERT INTO ihale_kalem (ikn, sira, ad, ad_norm, miktar, birim) VALUES (?, ?, ?, ?, ?, ?)', ); const ozet = d.prepare( `INSERT INTO kalem_kaynak (ikn, kalem_sayisi, dosya, yontem, ekleyen, zaman) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(ikn) DO UPDATE SET kalem_sayisi = excluded.kalem_sayisi, dosya = excluded.dosya, yontem = excluded.yontem, ekleyen = excluded.ekleyen, zaman = excluded.zaman`, ); d.exec('BEGIN'); try { sil.run(ikn); for (const k of kalemler) { ekle.run(ikn, k.sira ?? null, String(k.ad).slice(0, 300), normalizeTr(k.ad).slice(0, 300), k.miktar ?? null, k.birim ? String(k.birim).slice(0, 30) : null); } ozet.run(ikn, kalemler.length, dosya || null, yontem || null, ekleyen || null, new Date().toISOString()); d.exec('COMMIT'); } catch (e) { d.exec('ROLLBACK'); throw e; } return kalemler.length; } export function kalemOku(ikn) { const d = open(); if (!d || !ikn) return { kalemler: [], kaynak: null }; const kalemler = d.prepare( 'SELECT sira, ad, miktar, birim FROM ihale_kalem WHERE ikn = ? ORDER BY COALESCE(sira, 999999), id', ).all(ikn); const kaynak = d.prepare('SELECT kalem_sayisi, dosya, yontem, zaman FROM kalem_kaynak WHERE ikn = ?').get(ikn) || null; return { kalemler, kaynak }; } /** Havuzdaki tüm ihalelerin kalem özeti: ikn → { sayi, ilkler[] } */ export function kalemOzetleri(limit = 20000) { const d = open(); if (!d) return new Map(); const ozet = d.prepare('SELECT ikn, kalem_sayisi FROM kalem_kaynak LIMIT ?').all(limit); const harita = new Map(); if (!ozet.length) return harita; const ilk = d.prepare('SELECT ad FROM ihale_kalem WHERE ikn = ? ORDER BY COALESCE(sira, 999999), id LIMIT 6'); for (const o of ozet) { harita.set(o.ikn, { sayi: o.kalem_sayisi, ilkler: ilk.all(o.ikn).map((r) => r.ad) }); } return harita; } /** * Kalem metninde arama. Firmanın kendi ürününü ("eldiven", "ortopedik vida") * hangi ihalelerde geçtiğini bulması için. */ export function kalemAra(q, { limit = 400 } = {}) { const d = open(); const terim = normalizeTr(q || '').trim(); if (!d || terim.length < 2) return []; const satirlar = d.prepare( `SELECT ikn, ad, miktar, birim FROM ihale_kalem WHERE ad_norm LIKE ? ORDER BY ikn LIMIT ?`, ).all(`%${terim}%`, limit * 5); const harita = new Map(); for (const s of satirlar) { if (!harita.has(s.ikn)) harita.set(s.ikn, { ikn: s.ikn, eslesen: [] }); const e = harita.get(s.ikn); if (e.eslesen.length < 5) e.eslesen.push({ ad: s.ad, miktar: s.miktar, birim: s.birim }); if (harita.size >= limit) break; } return [...harita.values()]; } export function kalemSayilari() { const d = open(); if (!d) return { ihale: 0, kalem: 0 }; return { ihale: d.prepare('SELECT COUNT(*) n FROM kalem_kaynak').get().n, kalem: d.prepare('SELECT COUNT(*) n FROM ihale_kalem').get().n, }; }