Scope tender document workflows to the active account and tender
This commit is contained in:
@@ -555,3 +555,11 @@ Yeniden çıkarımda kullanıcı iptali gerçek okuma hatası gibi kaydedilmez.
|
||||
Test: gerçek yardımcı fonksiyon üzerinden iptal edilen DOCX okumasında yalnız bir metin isteği, sıfır hata kaydı/kalem yayını; hesap değişimi sonrasında gelen özel Office metninin reddi. Diğer belge ekranları ve çalışan Office sunucu çıkarımının iptali bu turda tamamlanmadı; tarayıcıdan depo kapanış kabulü açık.
|
||||
|
||||
Sonuç: Tüm 124 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Plan henüz tamamlanmadı; canlı yayın yapılmadı.
|
||||
|
||||
### D01 / D06 — İhale belge panelinin işlem yaşam süresi
|
||||
|
||||
TenderDocuments hesabı ve İKN'yi anahtar alan ayrı oturum bileşenine taşındı. Bileşen kapanırken PDF/metin okuma sinyali iptal edilir; panel durum güncellemeleri başlangıç oturumu ve etkin görünümle sınırlandırılır. Önizleme, yeniden çıkarım, tek belge analizi ve birleşik evrak okuma aynı sinyali kullanır. Toplu okumada iptal “atlanmış belge” olarak yutulmaz, kalan belgeler ve yeni AI başlatma durur.
|
||||
|
||||
PDF yüklemesinden sonra hesap/ihale değişirse çıkarım ve ortak kalem yayınına geçilmez. İptal, okuma hatası kaydı oluşturmaz. AI penceresi açılmadan ve incelenmiş metin analizinden sonra etkin oturum yeniden kontrol edilir. İlan metni edinimi de iptal sinyalini alır. Önceden başlamış AppContext genel AI çağrısının tam iptali ayrı iştir; bu değişiklik başlamış her sunucu işlemini durdurduğu iddiasını taşımaz.
|
||||
|
||||
Doğrulama: mevcut 124 test ve üretim derlemesi başarılı. Bu tur yeni birim test eklenmedi; önceki okuyucu/oturum testlerinin kapsadığı yardımcıların panel bağlantıları tamamlandı. İhale değişimi sırasında gerçek OCR ve toplu okuma tarayıcı kabulü açık. Büyük paket uyarıları sürüyor. Canlı yayın yapılmadı.
|
||||
|
||||
@@ -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';
|
||||
@@ -14,29 +14,49 @@ import { extractText } from '../services/pdfText';
|
||||
* - İlan metnini (ilan + OKAS kalemleri) çeker ve doğrudan AI analizine verir
|
||||
*/
|
||||
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