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 } 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 { analyzeSpecPdfWithGemini, setIsAiSpecAnalyzerOpen } = useApp(); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const [bilgi, setBilgi] = useState(null); const [ilerleme, setIlerleme] = useState(''); // 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] = useState(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] = 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; 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. const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn); let kalem = cevap?.kalem?.sayi || null; if (!kalem && ikn && /\.(pdf|txt|md)$/i.test(f.name)) { try { const { text } = await extractText(f); const { kalemCikar } = await import('../services/kalem'); const d = await kalemCikar({ ikn, metin: text, dosya: f.name }); kalem = d.sayi || null; } catch { /* kalem çıkmazsa yükleme yine de başarılı sayılır */ } } belgeleriYukle(); setBilgi({ yuklendi: f.name, kalem }); } 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); } }; // PDF/metin tarayıcıda (pdfjs) okunur; docx/xlsx/zip için tarayıcıda çözücü // yok, sunucudaki çıkarıcıya sorulur. .doc ve .rar'ın karşılığı yok. const tarayicidaOkunur = (ad) => /\.(pdf|txt|md)$/i.test(String(ad || '')); const sunucudaOkunur = (ad) => /\.(docx|xlsx|zip)$/i.test(String(ad || '')); const okunabilirMi = (ad) => tarayicidaOkunur(ad) || sunucudaOkunur(ad); const belgeMetni = async (b) => { if (sunucudaOkunur(b.ad)) { const r = await fetch(`/api/me/documents/text?id=${encodeURIComponent(b.id)}`, { headers: { Authorization: `Bearer ${getToken()}` }, }); const d = await r.json().catch(() => ({})); if (!d.success) throw new Error(d.error || 'metin çıkarılamadı'); return d.metin; } 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(); // extractText { text, pageCount, usedPages } döndürür — nesneyi metin sanıp // göndermek analizi sessizce bozuyordu. const { text } = await extractText(new File([blob], b.ad, { type: b.mime || 'application/pdf' })); return text; }; // 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 metin = await belgeMetni(b); 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, 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 () => { 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); 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 }); 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 = []; 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 { atlanan.push('EKAP ilan metni (alınamadı)'); } } for (const b of belgeler) { if (!okunabilirMi(b.ad)) { atlanan.push(`${b.ad} (metin çıkarılamıyor)`); continue; } setIlerleme(`${b.ad} okunuyor…`); try { const t = await belgeMetni(b); if (t && t.length >= 200) parcalar.push(`===== BELGE: ${b.ad} =====\n${t}`); else atlanan.push(`${b.ad} (yeterli metin yok — taranmış olabilir)`); } catch (e) { atlanan.push(`${b.ad} (${e.message})`); } } return { metin: parcalar.join('\n\n'), parcaSayisi: parcalar.length, atlanan }; }; const derinAnalizEt = async () => { setBusy('derin'); setError(null); setBilgi(null); try { const { metin, parcaSayisi, atlanan } = 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(''); setIsAiSpecAnalyzerOpen(true); await analyzeSpecPdfWithGemini({ text: metin, fileName: `${ikn || 'ihale'} — ${parcaSayisi} evrak birlikte`, tender, mode: 'derin', }); } catch (e) { setError(e.message); } finally { setBusy(null); setIlerleme(''); } }; return (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.
{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.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.kalem ? <> Teklif cetvelinden {bilgi.kalem} kalem otomatik çıkarıldı; bu ihale artık kalemleriyle listeleniyor.> : <> Bu dosyadan teklif cetveli kalemi çıkarılamadı.>} > )}
)} {error && ({error}
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.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'}
)}