perf: DT detaylari 10 paralel + adaptif fren (0.36 -> 3.2 kayit/sn) & feat: EKAP'tan inen dokumani ihaleye yukleme
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { getToken } from '../services/auth';
|
||||
import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download } from 'lucide-react';
|
||||
import { FileText, ExternalLink, Loader2, Sparkles, AlertTriangle, Download, Upload, Trash2, Paperclip } from 'lucide-react';
|
||||
import { uploadDocument, listTenderDocuments, deleteDocument } from '../services/account';
|
||||
import { extractText } from '../services/pdfText';
|
||||
|
||||
/**
|
||||
* İhale dokümanları paneli.
|
||||
@@ -15,6 +17,53 @@ export const TenderDocuments = ({ tender }) => {
|
||||
const [bilgi, setBilgi] = useState(null);
|
||||
|
||||
const ikn = tender?.ikn;
|
||||
const [belgeler, setBelgeler] = useState([]);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const dosyaRef = useRef(null);
|
||||
|
||||
const belgeleriYukle = useCallback(() => {
|
||||
if (!ikn) return;
|
||||
listTenderDocuments(ikn).then(setBelgeler).catch(() => {});
|
||||
}, [ikn]);
|
||||
useEffect(() => { belgeleriYukle(); }, [belgeleriYukle]);
|
||||
|
||||
// EKAP dokümanı kullanıcının tarayıcısına iniyor; indirdiği dosyayı buraya
|
||||
// yükleyince ihaleye bağlanır ve metni AI analizine verilebilir.
|
||||
const dosyaSec = async (e) => {
|
||||
const f = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!f) return;
|
||||
setYukleniyor(true); setError(null);
|
||||
try {
|
||||
await uploadDocument(f, 'İhale Dokümanı', ikn);
|
||||
belgeleriYukle();
|
||||
setBilgi({ yuklendi: f.name });
|
||||
} catch (err) {
|
||||
setError(err.kod === 'KOTA_ASILDI' ? err.message : `Yüklenemedi: ${err.message}`);
|
||||
} finally { setYukleniyor(false); }
|
||||
};
|
||||
|
||||
const belgeSil = async (id) => {
|
||||
try { await deleteDocument(id); belgeleriYukle(); } catch (err) { setError(err.message); }
|
||||
};
|
||||
|
||||
// Yüklenen PDF'i AI'ya ver
|
||||
const belgeyiAnalizEt = async (b) => {
|
||||
setBusy('belge'); setError(null);
|
||||
try {
|
||||
const r = await fetch(`/api/me/documents/download?id=${encodeURIComponent(b.id)}`, {
|
||||
headers: { Authorization: `Bearer ${getToken()}` },
|
||||
});
|
||||
if (!r.ok) throw new Error('Belge indirilemedi');
|
||||
const blob = await r.blob();
|
||||
const metin = await extractText(new File([blob], b.ad, { type: b.mime || 'application/pdf' }));
|
||||
if (!metin || metin.length < 100) throw new Error('Belgeden analiz için yeterli metin çıkarılamadı (taranmış PDF olabilir).');
|
||||
setIsAiSpecAnalyzerOpen(true);
|
||||
await analyzeSpecPdfWithGemini({ text: metin, fileName: b.ad, tender });
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally { setBusy(null); }
|
||||
};
|
||||
const ekapIhalesi = /^\d{4}\/\d+$/.test(String(ikn || ''));
|
||||
|
||||
const dokumaniAc = async () => {
|
||||
@@ -94,7 +143,8 @@ export const TenderDocuments = ({ tender }) => {
|
||||
{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.</>}
|
||||
{bilgi.karakter != null && <>{bilgi.ilanSayisi} ilan · {bilgi.okasSayisi} kalem · {bilgi.karakter.toLocaleString('tr-TR')} karakter metin analize gönderildi. </>}
|
||||
{bilgi.yuklendi && <><strong>{bilgi.yuklendi}</strong> yüklendi ve bu ihaleye bağlandı.</>}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -105,13 +155,66 @@ export const TenderDocuments = ({ tender }) => {
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* EKAP dokümanı kullanıcının tarayıcısına iniyor; sunucu tarafından
|
||||
alınamıyor. İndirilen dosya buraya yüklenip ihaleye bağlanır. */}
|
||||
<div className="border border-dashed border-slate-300 rounded-xl p-3 bg-slate-50/60">
|
||||
<p className="text-[11px] text-slate-600 leading-relaxed flex items-start gap-1.5">
|
||||
<Download className="w-3.5 h-3.5 shrink-0 mt-0.5 text-slate-400" />
|
||||
<span>
|
||||
EKAP dokümanı kendi oturumunuz üzerinden <strong>bilgisayarınıza iner</strong> — biz sunucudan
|
||||
alamıyoruz. İnen dosyayı buraya yükleyin; bu ihaleye bağlanır ve AI ile analiz edebilirsiniz.
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<input
|
||||
ref={dosyaRef}
|
||||
type="file"
|
||||
onChange={dosyaSec}
|
||||
accept=".pdf,.doc,.docx,.zip,.rar,.txt"
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={() => dosyaRef.current?.click()}
|
||||
disabled={yukleniyor}
|
||||
className="bg-slate-900 hover:bg-slate-800 disabled:opacity-40 text-white font-bold text-xs px-3.5 py-2 rounded-lg flex items-center gap-2"
|
||||
>
|
||||
{yukleniyor ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
|
||||
{yukleniyor ? 'Yükleniyor…' : 'İndirdiğim dosyayı yükle'}
|
||||
</button>
|
||||
<span className="text-[10px] text-slate-400">PDF, Word, ZIP · en fazla 10 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{belgeler.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[11px] font-bold text-slate-700 flex items-center gap-1.5">
|
||||
<Paperclip className="w-3.5 h-3.5 text-slate-400" /> Bu ihaleye yüklenen belgeler ({belgeler.length})
|
||||
</p>
|
||||
{belgeler.map((b) => (
|
||||
<div key={b.id} className="flex items-center gap-2 bg-white border border-slate-200 rounded-lg px-2.5 py-2">
|
||||
<FileText className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||
<span className="text-xs text-slate-800 truncate flex-1" title={b.ad}>{b.ad}</span>
|
||||
<span className="text-[10px] text-slate-400 whitespace-nowrap">
|
||||
{(b.boyut / 1024).toLocaleString('tr-TR', { maximumFractionDigits: 0 })} KB
|
||||
</span>
|
||||
<button
|
||||
onClick={() => belgeyiAnalizEt(b)}
|
||||
disabled={busy !== null}
|
||||
className="text-[10px] font-bold text-amber-700 hover:text-amber-800 disabled:opacity-40 whitespace-nowrap"
|
||||
>
|
||||
{busy === 'belge' ? '…' : 'AI analiz'}
|
||||
</button>
|
||||
<button onClick={() => belgeSil(b.id)} className="text-slate-300 hover:text-red-600" aria-label="Sil">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user