Share stored document retry flow across tender detail and vault
This commit is contained in:
@@ -307,3 +307,9 @@ Doğrulama: üretim derlemesi geçti; HTTP testi, başarısız çıkarım ve yen
|
||||
İhale detayındaki yükleme ve yeniden kalem çıkarımı, dosya metni okunamadığında sahiplik kontrollü read-error uç noktasına hata bildiriyor. Sunucu serbest hata metni veya başarı durumu kabul etmiyor; sabit, kullanıcıya yönelik okuma hatasını saklıyor. İstemcinin gördüğü önceki işleme durumu atomik UPDATE koşuluyla karşılaştırılıyor; gecikmiş hata bildirimi daha yeni sonucu ezemiyor (409). Bildirim kaydedilemezse arayüz bunu ayrıca belirtiyor.
|
||||
|
||||
70 test geçti. HTTP kapsamında oturumsuz/başka hesap/firma belgesi reddi, kalıcı hata ve eski durumla tekrar bildirimin 409 olması doğrulandı. Mevcut dosya ve baytları sonraki çıkarım denemelerinde korunuyor. Gerçek PDF ile tarayıcı kabulü ve genel depoya yeniden işleme entegrasyonu açık; bu değişiklik tam D01 kabulü sayılmıyor.
|
||||
|
||||
### Genel belge deposunda yeniden işleme
|
||||
|
||||
Doküman Deposu'nda İKN bağlantılı, okunabilir ihale belgelerine yeniden kalem çıkarımı eklendi. İhale detayı ve depo artık aynı storedDocument servisiyle mevcut dosyayı okuyor, okuma hatasını kaydediyor ve sahiplik kontrollü kalem aktarımı yapıyor. Özel firma belgeleri bu işleme dahil değil. İşlem sonucu ve liste yenileme hatası ayrı gösteriliyor. Belge okuma sırasında oturum değişirse eski belge metninin yeni oturumla aktarımı veya hata bildirimi durduruluyor.
|
||||
|
||||
71 test geçti; üretim derlemesi geçti. Yeni servis testi mevcut dosya ID/İKN/metin aktarımını, okuma hatasının önceki durumla raporlanmasını ve hesap değişikliğinde ikinci isteğin gönderilmemesini doğruluyor. Gerçek tarayıcı PDF kabulü ve tüm depo işlemlerinin hesap değişiminde ekran durumlarını temizlemesi henüz açık.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readableDocument, readStoredDocument, retryDocumentExtraction } from '../services/storedDocument';
|
||||
import { documentProcessingLabel } from '../services/documentProcessing';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
@@ -88,44 +89,13 @@ export const TenderDocuments = ({ tender }) => {
|
||||
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;
|
||||
};
|
||||
const okunabilirMi = readableDocument;
|
||||
const belgeMetni = readStoredDocument;
|
||||
|
||||
const kalemleriYenidenCikar = async (b) => {
|
||||
setBusy('kalem'); setError(null); setBilgi(null);
|
||||
try {
|
||||
let metin;
|
||||
try { metin = await belgeMetni(b); }
|
||||
catch (readError) {
|
||||
try { await reportDocumentReadError(b.id,b.isleme); }
|
||||
catch { throw new Error(`${readError.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); }
|
||||
throw readError;
|
||||
}
|
||||
const { kalemCikar } = await import('../services/kalem');
|
||||
const result = await kalemCikar({ ikn, metin, dosya:b.ad, documentId:b.id });
|
||||
const result = await retryDocumentExtraction(b);
|
||||
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); }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readableDocument, retryDocumentExtraction } from '../../services/storedDocument';
|
||||
import { documentProcessingLabel } from '../../services/documentProcessing';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
@@ -16,6 +17,7 @@ export const DocumentVault = () => {
|
||||
const [durum, setDurum] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [hata, setHata] = useState(null);
|
||||
const [islemeBilgi, setIslemeBilgi] = useState(null);
|
||||
const [kategori, setKategori] = useState(KATEGORILER[0]);
|
||||
const dosyaRef = useRef(null);
|
||||
|
||||
@@ -26,6 +28,19 @@ export const DocumentVault = () => {
|
||||
};
|
||||
useEffect(yukle, [authUser]);
|
||||
|
||||
const yenidenIsle = async (document) => {
|
||||
setBusy(true); setHata(null); setIslemeBilgi(null);
|
||||
try {
|
||||
const result = await retryDocumentExtraction(document);
|
||||
setIslemeBilgi(`${document.ad}: ${result.sayi || 0} kalem çıkarıldı.`);
|
||||
} catch (error) { setHata(error.message); }
|
||||
finally {
|
||||
try { setDurum(await listDocuments()); }
|
||||
catch { setHata(previous => [previous, 'Belge listesi yenilenemedi.'].filter(Boolean).join(' ')); }
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const dosyaSecildi = async (e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
@@ -250,6 +265,7 @@ export const DocumentVault = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{islemeBilgi && <p role="status" className="text-xs text-slate-700">{islemeBilgi}</p>}
|
||||
{durum.belgeler.map((d) => (
|
||||
<div key={d.id} className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
@@ -273,7 +289,14 @@ export const DocumentVault = () => {
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{d.tur === 'ihale' && d.ikn && readableDocument(d.ad) && (
|
||||
<button onClick={() => yenidenIsle(d)} disabled={busy || !d.erisilebilir}
|
||||
aria-label={`${d.ad} için kalem çıkarımını yeniden dene`}
|
||||
className="text-[10px] text-blue-700 font-bold disabled:opacity-40">
|
||||
Kalemleri yeniden çıkar
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => downloadDocument(d.id, d.ad).catch((e) => setHata(e.message))}
|
||||
className="text-slate-400 hover:text-teal-600 p-1" title="İndir">
|
||||
<Download className="w-4 h-4" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Sunucu tarafı hesap servisleri: plan/kota, kullanıcı verisi, API anahtarları, belgeler.
|
||||
* (Önceden bunların hepsi localStorage'daydı veya sahteydi.)
|
||||
*/
|
||||
import { getToken } from './auth';
|
||||
import { getToken } from './auth.js';
|
||||
|
||||
const auth = () => ({ Authorization: `Bearer ${getToken()}`, 'Content-Type': 'application/json' });
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* korunuyor). Bir kullanıcı dokümanı yükleyince kalemler havuza yazılıyor ve
|
||||
* o ihale herkesin kartında kalemleriyle görünüyor.
|
||||
*/
|
||||
import { getToken } from './auth';
|
||||
import { getToken } from './auth.js';
|
||||
|
||||
/** Bir ihalenin tüm kalemleri. */
|
||||
export async function kalemleriGetir(ikn) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { getToken } from './auth.js';
|
||||
import { reportDocumentReadError } from './account.js';
|
||||
|
||||
export const readableDocument = (name) => /\.(pdf|txt|md|docx|xlsx|zip)$/i.test(String(name || ''));
|
||||
|
||||
export async function readStoredDocument(document) {
|
||||
if (!readableDocument(document.ad)) throw new Error('Bu dosya biçiminden metin çıkarımı desteklenmiyor.');
|
||||
const office = /\.(docx|xlsx|zip)$/i.test(document.ad);
|
||||
const response = await fetch(`/api/me/documents/${office ? 'text' : 'download'}?id=${encodeURIComponent(document.id)}`, {
|
||||
headers: {Authorization:`Bearer ${getToken()}`},
|
||||
});
|
||||
if (office) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result.success) throw new Error(result.error || 'Belge metni alınamadı.');
|
||||
return result.metin;
|
||||
}
|
||||
if (!response.ok) throw new Error('Belge indirilemedi.');
|
||||
const blob = await response.blob();
|
||||
const { extractText } = await import('./pdfText');
|
||||
return (await extractText(new File([blob],document.ad,{type:document.mime || 'application/octet-stream'}))).text;
|
||||
}
|
||||
|
||||
export async function retryDocumentExtraction(document) {
|
||||
if (document.tur !== 'ihale' || !document.ikn) throw new Error('İhaleye bağlı ihale dokümanı gerekli.');
|
||||
const token = getToken();
|
||||
const checkSession = () => { if (!token || token !== getToken()) throw new Error('Oturum değişti; belgeyi yeniden açın.'); };
|
||||
checkSession();
|
||||
let text;
|
||||
try { text = await readStoredDocument(document); }
|
||||
catch (error) {
|
||||
checkSession();
|
||||
try { await reportDocumentReadError(document.id,document.isleme); }
|
||||
catch { throw new Error(`${error.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); }
|
||||
throw error;
|
||||
}
|
||||
checkSession();
|
||||
const { kalemCikar } = await import('./kalem.js');
|
||||
checkSession();
|
||||
return kalemCikar({ikn:document.ikn,metin:text,dosya:document.ad,documentId:document.id});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {retryDocumentExtraction} from '../src/services/storedDocument.js';
|
||||
|
||||
test('stored document retry uses owned file ID and stops before a changed session can publish',async()=>{
|
||||
const originalFetch=globalThis.fetch;
|
||||
const storage=Object.getOwnPropertyDescriptor(globalThis,'localStorage');
|
||||
let token='account-a';
|
||||
Object.defineProperty(globalThis,'localStorage',{configurable:true,value:{getItem:()=>token}});
|
||||
const doc={id:'owned-doc',ad:'cetvel.docx',ikn:'2026/123',tur:'ihale',isleme:{status:'awaiting_text'}};
|
||||
const calls=[];
|
||||
try {
|
||||
globalThis.fetch=async(url,options)=>{
|
||||
calls.push({url,options});
|
||||
if(url.includes('/text?')) return Response.json({success:true,metin:'Source table'});
|
||||
return Response.json({success:true,sayi:2});
|
||||
};
|
||||
assert.equal((await retryDocumentExtraction(doc)).sayi,2);
|
||||
const payload=JSON.parse(calls[1].options.body);
|
||||
assert.equal(payload.documentId,doc.id);assert.equal(payload.ikn,doc.ikn);assert.equal(payload.metin,'Source table');
|
||||
calls.length=0;
|
||||
globalThis.fetch=async(url)=>{calls.push(url);token='account-b';return Response.json({success:true,metin:'old account document'});};
|
||||
await assert.rejects(retryDocumentExtraction(doc),/Oturum değişti/);
|
||||
assert.equal(calls.length,1);
|
||||
token='account-a';calls.length=0;
|
||||
globalThis.fetch=async(url,options)=>{
|
||||
calls.push({url,options});
|
||||
return url.includes('/text?') ? Response.json({success:false,error:'Okunamadı'},{status:422}) : Response.json({success:true});
|
||||
};
|
||||
await assert.rejects(retryDocumentExtraction(doc),/Okunamadı/);
|
||||
assert.equal(calls[1].url,'/api/me/documents/read-error');
|
||||
assert.deepEqual(JSON.parse(calls[1].options.body),{id:doc.id,expectedProcessing:doc.isleme});
|
||||
assert.equal(calls.length,2);
|
||||
} finally {
|
||||
globalThis.fetch=originalFetch;
|
||||
if(storage) Object.defineProperty(globalThis,'localStorage',storage);else delete globalThis.localStorage;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user