Scope tender document workflows to the active account and tender
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { DocumentTextReview } from './DocumentTextReview';
|
||||
import { readableDocument, readStoredDocument, inspectStoredDocument, retryDocumentExtraction } from '../services/storedDocument';
|
||||
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';
|
||||
@@ -13,30 +13,50 @@ import { extractText } from '../services/pdfText';
|
||||
* - 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 }) => {
|
||||
export const TenderDocuments = ({tender}) => {
|
||||
const {authUser} = useApp();
|
||||
return <TenderDocumentSession key={`${authUser?.email || ''}:${tender.ikn || tender.id}`} tender={tender} />;
|
||||
};
|
||||
const TenderDocumentSession = ({ tender }) => {
|
||||
const { analyzeSpecPdfWithGemini, setIsAiSpecAnalyzerOpen } = useApp();
|
||||
const [busy, setBusy] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [bilgi, setBilgi] = useState(null);
|
||||
const [ilerleme, setIlerleme] = useState('');
|
||||
const [metinOnizleme, setMetinOnizleme] = useState(null);
|
||||
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] = useState(null);
|
||||
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] = useState([]);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const [listeHata, setListeHata] = useState(null);
|
||||
const [listeBekliyor, setListeBekliyor] = useState(false);
|
||||
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) return;
|
||||
if (!ikn || !current()) return;
|
||||
const request = ++listeIstegi.current;
|
||||
setListeBekliyor(true); setListeHata(null);
|
||||
try {
|
||||
@@ -64,19 +84,24 @@ export const TenderDocuments = ({ tender }) => {
|
||||
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)); }
|
||||
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; }
|
||||
@@ -93,14 +118,13 @@ export const TenderDocuments = ({ tender }) => {
|
||||
};
|
||||
|
||||
const okunabilirMi = readableDocument;
|
||||
const belgeMetni = readStoredDocument;
|
||||
|
||||
const metniIncele = async (b) => {
|
||||
setBusy('metin'); setError(null); setMetinOnizleme(null);
|
||||
const token = getToken();
|
||||
const request = ++onizlemeIstegi.current;
|
||||
try {
|
||||
const result = await inspectStoredDocument(b);
|
||||
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); }
|
||||
@@ -110,7 +134,7 @@ export const TenderDocuments = ({ tender }) => {
|
||||
const kalemleriYenidenCikar = async (b) => {
|
||||
setBusy('kalem'); setError(null); setBilgi(null);
|
||||
try {
|
||||
const result = await retryDocumentExtraction(b);
|
||||
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); }
|
||||
@@ -121,9 +145,10 @@ export const TenderDocuments = ({ tender }) => {
|
||||
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);
|
||||
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) {
|
||||
@@ -161,10 +186,13 @@ export const TenderDocuments = ({ tender }) => {
|
||||
};
|
||||
|
||||
const ilanMetniGetir = async () => {
|
||||
assertCurrent();
|
||||
const r = await fetch(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`, {
|
||||
headers: { Authorization: `Bearer ${getToken()}` },
|
||||
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;
|
||||
};
|
||||
@@ -175,6 +203,7 @@ export const TenderDocuments = ({ tender }) => {
|
||||
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) {
|
||||
@@ -200,21 +229,24 @@ export const TenderDocuments = ({ tender }) => {
|
||||
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);
|
||||
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});
|
||||
}
|
||||
@@ -232,6 +264,7 @@ export const TenderDocuments = ({ tender }) => {
|
||||
}
|
||||
setBilgi({ derin: { parcaSayisi, karakter: metin.length, atlanan } });
|
||||
setIlerleme('');
|
||||
assertCurrent();
|
||||
setIsAiSpecAnalyzerOpen(true);
|
||||
await analyzeSpecPdfWithGemini({
|
||||
text: metin,
|
||||
@@ -421,7 +454,9 @@ export const TenderDocuments = ({ tender }) => {
|
||||
</p>
|
||||
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words text-xs">{metinOnizleme.text || 'Okunabilir metin bulunamadı.'}</pre>
|
||||
{metinOnizleme.type === 'ihale' && <DocumentTextReview key={metinOnizleme.documentId} documentId={metinOnizleme.documentId} extractedText={metinOnizleme.text} onAnalyze={async(reviewedSource)=>{
|
||||
assertCurrent();
|
||||
await analyzeSpecPdfWithGemini({reviewedSource,tender,fileName:metinOnizleme.name});
|
||||
assertCurrent();
|
||||
setIsAiSpecAnalyzerOpen(true);
|
||||
}} />}
|
||||
<button onClick={()=>setMetinOnizleme(null)} className="text-xs underline">Metin önizlemesini kapat</button>
|
||||
|
||||
Reference in New Issue
Block a user