Firmalar ihalenin sadece basligina bakarak kendi urunlerinin icinde olup
olmadigini goremiyordu ("148 KALEM KIRTASIYE MALZEME ALIMI"). Artik bir
kullanici ihale dokumanini yukledinde teklif cetveli kalemleri ayristirilip
IKN'ye baglaniyor ve o ihale HERKESIN kartinda kalemleriyle gorunuyor.
NEDEN YUKLEMEYE DAYALI: kalem listesi EKAP'in programatik yuzeyinde yok.
- Arama/detay API'si (ihtiyacKalemiOkasList) yalnizca OKAS sinifi veriyor:
"KALP AKCIGER POMPASI CIHAZI ALIMI" icin tek satir "Tibbi cihazlar"
- Ilan metni yalnizca sayiyi veriyor: "148 KALEM ... Ayrintili bilgiye EKAP'ta
yer alan ihale dokumani icinde bulunan idari sartnameden ulasilabilir"
- Dokuman sayfasi CAPTCHA ile korunuyor (ctl00_btnCaptchaProtect, guvenlik
resmi). Gercek tarayiciyla (Playwright) da acildi, donen tek metin: "Dogrula"
OTOMATIK, AI'SIZ: kalem cikarimi yukleme aninda kendiliginden calisir,
kullanicidan tiklama beklemez ve model cagirmaz (saf regex). AI analizi ayri
dugmede, kullanici isterse.
- server/kalem-cikar.mjs: teklif cetveli ayristirici. " | " tablolarindan
(docx/xlsx/zip) sutun basligini cozerek; ayracsiz PDF metninde sezgisel
satir kalibiyla. Miktar+birim tek hucrede olabiliyor ("25000 TEST").
Birim listesinde 'test' var — tibbi kitlerde kalem miktari boyle yaziliyor.
- server/kalem-store.mjs: kalem.sqlite, IKN bazli, Turkce duyarli arama.
Bos ayristirma mevcut iyi veriyi silmez.
- Uclar: POST /api/kalem/cikar (tarayicidan gelen PDF metni), GET /api/kalem,
GET /api/kalem/ara, GET /api/health/kalem
- /api/tenders payloadu kalemSayisi + ilk kalemleri tasiyor
- Kartta kalem rozeti ve kalem seridi; aramada eslesen kalemler one alinir
- Ust arama kutusu kalem havuzunda da ariyor (350ms debounce, sunucu tarafi)
- Detay modalinda tam kalem listesi + kalem ici arama
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
5.1 KiB
JavaScript
158 lines
5.1 KiB
JavaScript
/**
|
||
* 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 = 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) return null;
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
db = new DatabaseSync(DB_PATH);
|
||
db.exec('PRAGMA journal_mode = WAL;');
|
||
db.exec(SCHEMA);
|
||
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,
|
||
};
|
||
}
|