import { DocumentTextReview } from './DocumentTextReview'; import { readableDocument, inspectStoredDocument, retryDocumentExtraction } from '../services/storedDocument'; import { documentProcessingLabel } from '../services/documentProcessing'; 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, Upload, Trash2, Paperclip, Route } from 'lucide-react'; import { uploadDocument, listTenderDocuments, deleteDocument, reportDocumentReadError } from '../services/account'; import { extractText } from '../services/pdfText'; /** * İ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 {authUser} = useApp(); return ; }; const TenderDocumentSession = ({ tender }) => { const { analyzeSpecPdfWithGemini, setIsAiSpecAnalyzerOpen } = useApp(); const readLifetime = useRef(new AbortController()); const sessionToken = useRef(getToken()); const current = () => !readLifetime.current.signal.aborted && Boolean(sessionToken.current) && sessionToken.current === getToken(); const assertCurrent = () => { readLifetime.current.signal.throwIfAborted(); if (!current()) throw new Error('Oturum değişti; belgeyi yeniden açın.'); }; useEffect(() => { readLifetime.current = new AbortController(); return () => readLifetime.current.abort(); }, []); const useOwnedState = initial => { const [value,setValue] = useState(initial); const setOwned = useCallback(next => {if(current())setValue(next);},[]); return [value,setOwned]; }; const [busy, setBusy] = useOwnedState(null); const [error, setError] = useOwnedState(null); const [bilgi, setBilgi] = useOwnedState(null); const [ilerleme, setIlerleme] = useOwnedState(''); const [metinOnizleme, setMetinOnizleme] = useOwnedState(null); // Son üretilen EKAP doküman adresi — yeni sekme engellenirse kullanıcı // gerçek bir bağlantıya tıklayarak açabilsin (bağlantı tıklaması engellenmez). const [ekapUrl, setEkapUrl] = useOwnedState(null); // Eski kayıtlarda İKN boşluklu gelebiliyor (" 2026/1622878"); EKAP tespiti // ve sunucu çağrıları boşluksuz değerle yapılır. const ikn = String(tender?.ikn || '').replace(/[\s\u00a0]+/g, '') || undefined; const [belgeler, setBelgeler] = useOwnedState([]); const [yukleniyor, setYukleniyor] = useOwnedState(false); const [listeHata, setListeHata] = useOwnedState(null); const [listeBekliyor, setListeBekliyor] = useOwnedState(false); const listeIstegi = useRef(0); const onizlemeIstegi = useRef(0); const dosyaRef = useRef(null); const belgeleriYukle = useCallback(async () => { if (!ikn || !current()) return; const request = ++listeIstegi.current; setListeBekliyor(true); setListeHata(null); try { const rows = await listTenderDocuments(ikn); if (request === listeIstegi.current) setBelgeler(rows); } catch (e) { if (request === listeIstegi.current) setListeHata(e.message); } finally { if (request === listeIstegi.current) setListeBekliyor(false); } }, [ikn]); useEffect(() => { setBelgeler([]); setMetinOnizleme(null); belgeleriYukle(); return () => { listeIstegi.current++; onizlemeIstegi.current++; }; }, [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; if (f.size > 10 * 1024 * 1024) { setError(`Yüklenemedi: ${f.name} ${(f.size / 1048576).toFixed(1)} MB — sınır 10 MB.`); return; } setYukleniyor(true); setError(null); try { // Sunucu docx/xlsx/zip için teklif cetveli kalemlerini yüklemede // otomatik çıkarır. PDF tarayıcıda çözüldüğü için metni biz göndeririz. assertCurrent(); const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, {documentType:'ihale'}); assertCurrent(); let kalem = cevap?.kalem?.sayi || null; let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null; if (!kalem && ikn && /\.(pdf|txt|md)$/i.test(f.name)) { try { let text; try { ({text} = await extractText(f,{signal:readLifetime.current.signal})); } catch (readError) { assertCurrent(); if(readError.name === 'AbortError')throw readError; try { await reportDocumentReadError(cevap.id,cevap.belgeler.find(b=>b.id===cevap.id)?.isleme); } catch { throw new Error(`${readError.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); } throw readError; } const { kalemCikar } = await import('../services/kalem'); assertCurrent(); const d = await kalemCikar({ ikn, metin: text, dosya: f.name, documentId:cevap.id }); kalem = d.sayi || null; } catch (error) { kalemHata = error.message; } } belgeleriYukle(); setBilgi({ yuklendi: f.name, kalem, kalemHata }); } 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); } }; const okunabilirMi = readableDocument; const metniIncele = async (b) => { setBusy('metin'); setError(null); setMetinOnizleme(null); const token = getToken(); const request = ++onizlemeIstegi.current; try { const result = await inspectStoredDocument(b,{signal:readLifetime.current.signal}); if (token !== getToken() || request !== onizlemeIstegi.current) return; setMetinOnizleme({...result,name:b.ad,documentId:b.id,type:b.tur}); } catch (e) { if (token === getToken() && request === onizlemeIstegi.current) setError(e.message); } finally { if (token === getToken() && request === onizlemeIstegi.current) setBusy(null); } }; const kalemleriYenidenCikar = async (b) => { setBusy('kalem'); setError(null); setBilgi(null); try { const result = await retryDocumentExtraction(b,{signal:readLifetime.current.signal}); setBilgi({ isleme: `${b.ad}: ${result.sayi || 0} kalem çıkarıldı.` }); } catch (e) { setError(`Kalem çıkarımı tamamlanamadı: ${e.message}. Dosya kaydı korunmuştur.`); } finally { await belgeleriYukle(); setBusy(null); } }; // Yüklenen PDF'i AI'ya ver const belgeyiAnalizEt = async (b, mod) => { setBusy('belge'); setError(null); try { if (!okunabilirMi(b.ad)) throw new Error('Bu dosya türünden metin çıkarılamıyor (PDF, DOCX, XLSX, ZIP ve TXT destekleniyor).'); const reading = await inspectStoredDocument(b,{signal:readLifetime.current.signal}); const metin = reading.text; if (!metin || metin.length < 100) throw new Error('Belgeden analiz için yeterli metin çıkarılamadı (taranmış PDF olabilir).'); assertCurrent(); setIsAiSpecAnalyzerOpen(true); await analyzeSpecPdfWithGemini({ text: metin, fileName: b.ad, extraction:{ocr:reading.ocr,pageCount:reading.pageCount,usedPages:reading.usedPages,complete:reading.complete,fileName:b.ad}, tender, mode: mod }); } catch (e) { setError(e.message); } finally { setBusy(null); } }; const ekapIhalesi = /^\d{4}\/\d+$/.test(String(ikn || '')); const dokumaniAc = async () => { setBusy('url'); setError(null); // Açılır pencere engelleyici: yeni sekme, tıklamanın HEMEN içinde // (await'ten önce) açılmalı. Sunucu yanıtından sonra çağrılan window.open // Safari, PWA ve sıkı ayarlı Chrome'da sessizce engelleniyor — kullanıcı // "düğme çalışmıyor / dokümanlar açılmıyor" görüyordu. Sekme boş açılır, // adres gelince yönlendirilir; engellenirse aşağıda gerçek bağlantı çıkar. let sekme = null; try { sekme = window.open('', '_blank'); } catch { sekme = null; } if (sekme) { try { sekme.opener = null; sekme.document.write('

EKAP doküman sayfasına yönlendiriliyor…

'); } catch { /* yazılamazsa da yönlendirme çalışır */ } } try { const r = await fetch(`/api/documents/url?ikn=${encodeURIComponent(ikn)}`); const d = await r.json(); if (!d.success) throw new Error(d.error); setEkapUrl(d.url); setBilgi({ dokumanSayisi: d.dokumanSayisi, ekapSekme: sekme ? 'acildi' : 'engellendi' }); if (sekme) sekme.location.href = d.url; } catch (e) { if (sekme) { try { sekme.close(); } catch { /* yoksay */ } } setError(e.message); } finally { setBusy(null); } }; const ilanMetniGetir = async () => { assertCurrent(); const r = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`, { signal:readLifetime.current.signal, headers: { Authorization: `Bearer ${sessionToken.current}` }, }); const d = await r.json(); assertCurrent(); if (!d.success) throw new Error(d.error); return d; }; const ilaniAnalizEt = async () => { setBusy('analiz'); setError(null); try { const d = await ilanMetniGetir(); 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 }); assertCurrent(); setIsAiSpecAnalyzerOpen(true); await analyzeSpecPdfWithGemini({ text: d.metin, fileName: `${d.ikn} — EKAP ilan metni`, tender }); } catch (e) { setError(e.message); } finally { setBusy(null); } }; /** * Derin analiz — teknik şartname ve diğer evrakın tamamını TEK metinde * birleştirip gönderir: EKAP ilan metni + bu ihaleye yüklenmiş her belge. * Böylece model şartnameyi ilanla ve eklerle çapraz okuyabiliyor. */ const tumEvrakiTopla = async () => { const parcalar = []; const atlanan = []; const extraction = []; if (ekapIhalesi) { setIlerleme('EKAP ilan metni alınıyor…'); try { const d = await ilanMetniGetir(); if (d.metin && d.metin.length >= 100) { parcalar.push(`===== EKAP İLAN METNİ (${d.ikn}) =====\n${d.metin}`); } } catch { assertCurrent(); atlanan.push('EKAP ilan metni (alınamadı)'); extraction.push({fileName:'EKAP ilan metni',complete:false}); } } for (const b of belgeler) { assertCurrent(); if (!okunabilirMi(b.ad)) { atlanan.push(`${b.ad} (metin çıkarılamıyor)`); extraction.push({fileName:b.ad,complete:false}); continue; } setIlerleme(`${b.ad} okunuyor…`); try { const reading = await inspectStoredDocument(b,{signal:readLifetime.current.signal}); const t = reading.text; extraction.push({ocr:reading.ocr,pageCount:reading.pageCount,usedPages:reading.usedPages,complete:reading.complete,fileName:b.ad}); if (t && t.length >= 200) parcalar.push(`===== BELGE: ${b.ad} =====\n${t}`); else { atlanan.push(`${b.ad} (yeterli metin yok — taranmış olabilir)`); extraction.push({fileName:b.ad,complete:false}); } } catch (e) { assertCurrent(); atlanan.push(`${b.ad} (${e.message})`); extraction.push({fileName:b.ad,complete:false}); } } return { metin: parcalar.join('\n\n'), parcaSayisi: parcalar.length, atlanan, extraction }; }; const derinAnalizEt = async () => { setBusy('derin'); setError(null); setBilgi(null); try { const { metin, parcaSayisi, atlanan, extraction } = await tumEvrakiTopla(); if (!metin || metin.length < 200) { throw new Error('Derin analiz için yeterli metin toplanamadı. EKAP dokümanını indirip PDF olarak yükleyin.'); } setBilgi({ derin: { parcaSayisi, karakter: metin.length, atlanan } }); setIlerleme(''); assertCurrent(); setIsAiSpecAnalyzerOpen(true); await analyzeSpecPdfWithGemini({ text: metin, extraction, fileName: `${ikn || 'ihale'} — ${parcaSayisi} evrak birlikte`, tender, mode: 'derin', }); } catch (e) { setError(e.message); } finally { setBusy(null); setIlerleme(''); } }; return (

İhale Dokümanları

{!ekapIhalesi ? (

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ı{' '} buradan açabilirsiniz . )}

) : ( <>

İ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; EKAP önce kısa bir güvenlik doğrulaması (resim seçme) ister, ardından şartname ve ekleri listelenir. İlan metnini ise analiz için buradan çekebilirsiniz.

{ekapUrl && (

{bilgi?.ekapSekme === 'engellendi' ? <>Tarayıcınız yeni sekmeyi engelledi. : <>EKAP doküman sayfası yeni sekmede açıldı. Açılmadıysa: } EKAP doküman sayfasını aç {' '}· Bağlantı bir süre sonra geçerliliğini yitirebilir; açılmazsa düğmeye yeniden basın.

)} {bilgi && !bilgi.derin && (

{bilgi.dokumanSayisi > 0 && <>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.yuklendi && ( <> {bilgi.yuklendi} yüklendi ve bu ihaleye bağlandı. {bilgi.kalemHata && Kalem aktarımı tamamlanamadı: {bilgi.kalemHata}. Dosya kaydı korunmuştur.} {bilgi.kalem ? <> Teklif cetvelinden {bilgi.kalem} kalem otomatik çıkarıldı; bu ihale artık kalemleriyle listeleniyor. : <> Bu dosyadan teklif cetveli kalemi çıkarılamadı.} )}

)} )} {/* 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. */}

EKAP dokümanı kendi oturumunuz üzerinden bilgisayarınıza iner — biz sunucudan alamıyoruz. İnen dosyayı buraya yükleyin; bu ihaleye bağlanır ve AI ile analiz edebilirsiniz.

PDF, DOCX, XLSX, ZIP · en fazla 10 MB
{error && (

{error}

)} {bilgi?.isleme &&

{bilgi.isleme}

} {listeBekliyor &&

Belge listesi yükleniyor…

} {listeHata &&
Belge listesi alınamadı: {listeHata}
} {belgeler.length > 0 && (

Bu ihaleye yüklenen belgeler ({belgeler.length})

{belgeler.map((b) => (
{b.ad}{documentProcessingLabel(b.isleme)}
{(b.boyut / 1024).toLocaleString('tr-TR', { maximumFractionDigits: 0 })} KB {okunabilirMi(b.ad) && } {b.tur === 'ihale' && okunabilirMi(b.ad) && ( )}
))}
)} {metinOnizleme &&
{metinOnizleme.name} — okunan metin

{metinOnizleme.pageCount != null ? `${metinOnizleme.usedPages}/${metinOnizleme.pageCount} sayfa okundu. ` : 'Sunucuda okunan belge metni. '} {metinOnizleme.ocr ? `OCR kullanılan sayfalar: ${metinOnizleme.pages.filter(p=>p.method === 'ocr').map(p=>p.page).join(', ')}. ` : ''} Metni resmi dokümanla karşılaştırın; okunmuş olması doğruluğunun onaylandığı anlamına gelmez.

{metinOnizleme.text || 'Okunabilir metin bulunamadı.'}
{metinOnizleme.type === 'ihale' && { assertCurrent(); await analyzeSpecPdfWithGemini({reviewedSource,tender,fileName:metinOnizleme.name}); assertCurrent(); setIsAiSpecAnalyzerOpen(true); }} />}
} {(ekapIhalesi || belgeler.length > 0) && (
Derin Analiz & Yol Haritası

EKAP ilan metni ve bu ihaleye yüklediğiniz tüm evrak birlikte okunur; elenme sebepleri, gözden kaçan ince detaylar, belge kontrol listesi, kritik tarihler ve teklife kadar adım adım yol haritası çıkarılır.

{bilgi?.derin ? (

{bilgi.derin.parcaSayisi} kaynak · {bilgi.derin.karakter.toLocaleString('tr-TR')} karakter analize gönderildi.

{bilgi.derin.atlanan.length > 0 && (

Atlanan: {bilgi.derin.atlanan.join(', ')}

)}
) : (

{belgeler.length > 0 ? `${ekapIhalesi ? 'İlan metni + ' : ''}${belgeler.length} yüklü belge okunacak` : 'Henüz belge yüklemediniz — yalnızca ilan metni okunacak'}

)}
)}
); };