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:
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { getToken } from '../services/auth';
|
||||
import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* İhale dokümanları paneli.
|
||||
* - EKAP doküman bağlantısını üretip kullanıcının tarayıcısında açar
|
||||
* - İlan metnini (ilan + OKAS kalemleri) çeker ve doğrudan AI analizine verir
|
||||
*/
|
||||
export const TenderDocuments = ({ tender }) => {
|
||||
const { analyzeSpecPdfWithGemini, setIsAiSpecAnalyzerOpen } = useApp();
|
||||
const [busy, setBusy] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [bilgi, setBilgi] = useState(null);
|
||||
|
||||
const ikn = tender?.ikn;
|
||||
const ekapIhalesi = /^\d{4}\/\d+$/.test(String(ikn || ''));
|
||||
|
||||
const dokumaniAc = async () => {
|
||||
setBusy('url'); setError(null);
|
||||
try {
|
||||
const r = await fetch(`/api/documents/url?ikn=${encodeURIComponent(ikn)}`);
|
||||
const d = await r.json();
|
||||
if (!d.success) throw new Error(d.error);
|
||||
setBilgi({ dokumanSayisi: d.dokumanSayisi });
|
||||
window.open(d.url, '_blank', 'noopener,noreferrer');
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally { setBusy(null); }
|
||||
};
|
||||
|
||||
const ilaniAnalizEt = async () => {
|
||||
setBusy('analiz'); setError(null);
|
||||
try {
|
||||
const r = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`, {
|
||||
headers: { Authorization: `Bearer ${getToken()}` },
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!d.success) throw new Error(d.error);
|
||||
if (!d.metin || d.metin.length < 100) throw new Error('İlan metni analiz için yeterli değil.');
|
||||
setBilgi({ karakter: d.metin.length, ilanSayisi: d.ilanSayisi, okasSayisi: d.okasSayisi });
|
||||
setIsAiSpecAnalyzerOpen(true);
|
||||
await analyzeSpecPdfWithGemini({ text: d.metin, fileName: `${d.ikn} — EKAP ilan metni`, tender });
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally { setBusy(null); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-5 rounded-xl border border-slate-200 space-y-3">
|
||||
<h4 className="font-bold text-sm text-slate-900 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-blue-600" /> İhale Dokümanları
|
||||
</h4>
|
||||
|
||||
{!ekapIhalesi ? (
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
Bu kayıt EKAP İKN formatında değil; doküman bağlantısı yalnızca EKAP ihaleleri için üretilebilir.
|
||||
{tender?.kaynakUrl && (
|
||||
<> İlanın kaynağını{' '}
|
||||
<a href={tender.kaynakUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 font-semibold hover:underline">
|
||||
buradan açabilirsiniz
|
||||
</a>.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
İhale dokümanları EKAP üzerinde tutulur. Aşağıdaki bağlantı sizi doğrudan bu ihalenin
|
||||
EKAP doküman sayfasına götürür; ilan metnini ise analiz için buradan çekebilirsiniz.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={dokumaniAc}
|
||||
disabled={busy !== null}
|
||||
className="bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white font-bold text-xs px-4 py-2.5 rounded-xl flex items-center gap-2"
|
||||
>
|
||||
{busy === 'url' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <ExternalLink className="w-3.5 h-3.5" />}
|
||||
EKAP Dokümanlarını Aç
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={ilaniAnalizEt}
|
||||
disabled={busy !== null}
|
||||
className="bg-amber-400 hover:bg-amber-300 disabled:opacity-40 text-slate-950 font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2"
|
||||
>
|
||||
{busy === 'analiz' ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Sparkles className="w-3.5 h-3.5" />}
|
||||
İlan Metnini AI ile Analiz Et
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{bilgi && (
|
||||
<p className="text-[11px] text-slate-500">
|
||||
{bilgi.dokumanSayisi != null && <>EKAP'ta {bilgi.dokumanSayisi} doküman kayıtlı. </>}
|
||||
{bilgi.karakter != null && <>{bilgi.ilanSayisi} ilan · {bilgi.okasSayisi} kalem · {bilgi.karakter.toLocaleString('tr-TR')} karakter metin analize gönderildi.</>}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
|
||||
<p className="text-[11px] text-red-800 leading-relaxed">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-[10px] text-slate-400 leading-relaxed flex items-start gap-1.5">
|
||||
<Download className="w-3 h-3 shrink-0 mt-0.5" />
|
||||
EKAP, doküman indirmeyi kendi oturumu üzerinden yaptırır; bağlantı sizin tarayıcınızda açılır.
|
||||
Şartname dosyanız elinizdeyse AI analiz penceresinden doğrudan yükleyebilirsiniz.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user