feat: ihale dokümanları + OKAS kalem verisi + doğrudan temin (Playwright) + kalıcı arşiv
1) İhale evrakları (yeni)
- server/documents.mjs: EKAP doküman URL üretimi (imzalı), ilan metni +
OKAS kalem çıkarımı, Playwright ile doküman metni (varsa)
- /api/documents/url · /ilan · /text · /config uçları
- TenderDocuments paneli: 'EKAP Dokümanlarını Aç' + 'İlan Metnini AI ile
Analiz Et' (11K+ karakter gerçek ilan metni doğrudan AI'ya gider)
- NOT: eski EKAP host'u bot isteklerine bağlantı kesiyor; bağlantı
kullanıcının tarayıcısında açılır, sunucu tarafı metin için Chromium gerekir
2) Kalem verisi gerçek oldu
- ekap-scan: her ihale için IhaleDetay'dan OKAS ihtiyaç kalemleri çekilir
- history-store: okas_kalemler kolonu + otomatik şema geçişi
- --kalem-doldur modu: mevcut kayıtlara IKN'den çözerek kalem ekler
- scheduler: tarama sonrası kalem doldurmayı zincirler
- PriceAnalysis: yöntem şeffaflığı notu (birim fiyatlar kamuya açık değil;
kategori bazlı yaklaşık değer olduğu açıkça yazıyor)
3) Doğrudan temin (Playwright)
- Dockerfile: node:22-slim + nginx + Playwright/Chromium
- scheduler: DT worker periyodik (DT_INTERVAL_SAAT, varsayılan 4 saat)
- Bellek koruması: boş bellek DT_MIN_BOS_MB altındaysa tarama atlanır ve
uyarı loglanır; DT_ENABLED=0 ile tamamen kapatılabilir
4) Kalıcı arşiv
- Coolify'da /app/server/data volume bağlandı; arşiv artık deploy'lar
arası korunuyor
Ayrıca: TenderDetailModal'daki sabit '3-5 Firma' tahmini kaldırıldı, yerine
gerçek geçerli teklif sayısı gösteriliyor.
This commit is contained in:
@@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS sonuclar (
|
||||
yaklasik_maliyet REAL,
|
||||
teklif_sayisi INTEGER,
|
||||
gecerli_teklif_sayisi INTEGER,
|
||||
okas_kalemler TEXT,
|
||||
cekilme_zamani TEXT,
|
||||
PRIMARY KEY (ikn, kisim)
|
||||
);
|
||||
@@ -67,6 +68,16 @@ export function openDb({ create = false } = {}) {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -74,12 +85,13 @@ 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,cekilme_zamani)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
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();
|
||||
@@ -92,6 +104,7 @@ export function saveResults(db, tender, results) {
|
||||
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++;
|
||||
@@ -106,6 +119,28 @@ export function hasIkn(db, ikn) {
|
||||
|
||||
// --- 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}`,
|
||||
@@ -125,7 +160,7 @@ function rowToTender(r) {
|
||||
teklifSayisi: r.teklif_sayisi,
|
||||
gecerliTeklifSayisi: r.gecerli_teklif_sayisi,
|
||||
durum: 'Sonuçlandı',
|
||||
items: [],
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user