Add source-comparison form for versioned document text reviews

This commit is contained in:
2026-09-06 09:48:09 +03:00
parent 559a2feeda
commit e49593be46
4 changed files with 79 additions and 1 deletions
+6
View File
@@ -359,3 +359,9 @@ Belge sahibinin OCR/metin incelemesini kaydetmesi için document_text_reviews ta
Doğrulama: mevcut 78 test ve üretim derlemesi geçti; ek kaynak-dosyası-değişimi testi de geçti (toplam 79 ayrı test). HTTP testinde sahiplik, zorunlu karşılaştırma, kalıcı okuma ve eski sürüm reddi; ek testte gerçek dosya baytları değişince incelemenin stale olması ve belge silinince geçmişin kaldırılması doğrulandı.
Bu yalnız sunucu altyapısıdır. Metin düzenleme arayüzü, bu kaydın analizde sunucu tarafından çözülmesi ve kontrollü hazırlık ilerlemesi henüz bağlanmadı; mevcut OCR engeli sürüyor. Kullanıcı beyanı hukuki veya otomatik doğruluk garantisi değildir. Gerçek resmi dosya kabulü açık.
### Metin inceleme formu
İhale dokümanı “Metni incele” paneline DocumentTextReview eklendi. Kaynakla karşılaştırılmış düzeltme metni, sayfa/düzeltme açıklaması ve tüm sayfaları karşılaştırma beyanı alınır. Metin veya not değişince beyan sıfırlanır. Başarısız kayıt taslağı korur; sürüm uyuşmazlığında kullanıcıya taslağını kopyalayıp güncel kaydı yükleme seçeneği sunulur. Son kayıt sürümü/zamanı ve kaynak değişikliği açık gösterilir. Kayıt başarısı sunucu yanıtıyla gösterilir; bileşen kapanması/oturum değişiminden sonraki yanıtlar uygulanmaz.
79 test ve üretim derlemesi geçti. Formun gerçek tarayıcı kabulü ve kaydedilen incelemenin analizde sunucu tarafından çözülmesi henüz tamamlanmadı. Form, kayıt işleminin tek başına hazır teklif üretmediğini açıkça belirtiyor; mevcut OCR engeli devam ediyor.
+65
View File
@@ -0,0 +1,65 @@
import React,{useEffect,useRef,useState} from 'react';
import {getDocumentTextReview,saveDocumentTextReview} from '../services/account';
import {getToken} from '../services/auth';
export function DocumentTextReview({documentId,extractedText}) {
const [source,setSource]=useState(null);
const [text,setText]=useState(extractedText || '');
const [note,setNote]=useState('');
const [compared,setCompared]=useState(false);
const [busy,setBusy]=useState(false);
const [error,setError]=useState(null);
const [saved,setSaved]=useState(null);
const live=useRef(true);
const sequence=useRef(0);
const load=async()=>{
const request=++sequence.current, token=getToken();
setBusy(true);setError(null);
try {
const data=await getDocumentTextReview(documentId);
if(!live.current || request!==sequence.current || token!==getToken())return;
setSource(data);
// A stale source must be compared afresh; do not seed from an old correction.
setText(!data.stale && data.review ? data.review.corrected_text : extractedText || '');
setNote(!data.stale && data.review ? data.review.review_note : '');
setCompared(false);setSaved(null);
} catch(e) {if(live.current && request===sequence.current && token===getToken())setError(e.message);}
finally {if(live.current && request===sequence.current && token===getToken())setBusy(false);}
};
useEffect(()=>{live.current=true;load();return()=>{live.current=false;sequence.current++;};},[documentId]);
const save=async()=>{
if(!source || busy)return;
const token=getToken();setBusy(true);setError(null);setSaved(null);
try {
const result=await saveDocumentTextReview(documentId,{fileHash:source.fileHash,
expectedRevision:source.review?.revision || 0,correctedText:text,note,allPagesCompared:compared});
if(!live.current || token!==getToken())return;
setSource(result);setSaved(result.review);setCompared(false);
} catch(e) {if(live.current && token===getToken())setError(e.message);}
finally {if(live.current && token===getToken())setBusy(false);}
};
return <section aria-label="Belge metni incelemesi" className="border-t pt-3 space-y-2">
<h5 className="font-bold text-sm">Metni karşılaştır ve düzelt</h5>
<p className="text-xs text-slate-600">Belgenin tüm sayfalarını açıp metin, miktar ve birimleri karşılaştırın. Bu kayıt kullanıcı incelemesidir; resmî onay veya e-imza değildir. Kaydetmek tek başına teklif dosyasını hazır yapmaz.</p>
{source?.stale && <p role="alert" className="text-xs text-amber-800">Kaynak dosya değişmiş. Önceki inceleme geçerli değil; tüm sayfaları yeniden karşılaştırın.</p>}
{source?.review && <p className="text-xs text-slate-500">Son kayıt: sürüm {source.review.revision} · {new Date(source.review.reviewed_at).toLocaleString('tr-TR')}</p>}
<label className="block text-xs font-semibold">Kontrol edilen metin
<textarea value={text} onChange={e=>{setText(e.target.value);setCompared(false);setSaved(null);}} disabled={busy}
className="block w-full h-64 border rounded p-2 font-mono font-normal mt-1" />
</label>
<label className="block text-xs font-semibold">Karşılaştırılan sayfalar ve düzeltmeler
<textarea value={note} onChange={e=>{setNote(e.target.value);setCompared(false);setSaved(null);}} disabled={busy}
placeholder="Örnek: Sayfa 1–3 karşılaştırıldı; 2. sayfadaki miktar 100 olarak düzeltildi."
className="block w-full border rounded p-2 font-normal mt-1" />
</label>
<label className="flex gap-2 text-xs"><input type="checkbox" checked={compared} onChange={e=>setCompared(e.target.checked)} disabled={busy}/>
Bu metni kaynak dosyanın tüm sayfalarıyla karşılaştırdım.</label>
<button onClick={save} disabled={busy || !source || !compared || text.trim().length<40 || note.trim().length<20}
className="text-xs bg-blue-700 text-white px-3 py-2 rounded disabled:opacity-40">{busy ? 'İşleniyor…' : 'İncelemeyi kaydet'}</button>
{error && <div role="alert" className="text-xs text-red-700">{error}
<p>Metin taslağınız bu ekranda korunuyor. Sürüm değiştiyse taslağı kopyalayıp güncel kaydı yükleyin.</p>
<button onClick={load} disabled={busy} className="underline">Güncel kaydı yükle (taslağı değiştirir)</button>
</div>}
{saved && <p role="status" className="text-xs text-green-800">İnceleme sürüm {saved.revision} sunucuda kaydedildi. Analiz ve teklif kontrolleri ayrıca tamamlanmalıdır.</p>}
</section>;
}
+3 -1
View File
@@ -1,3 +1,4 @@
import { DocumentTextReview } from './DocumentTextReview';
import { readableDocument, readStoredDocument, inspectStoredDocument, retryDocumentExtraction } from '../services/storedDocument';
import { documentProcessingLabel } from '../services/documentProcessing';
import React, { useState, useEffect, useRef, useCallback } from 'react';
@@ -101,7 +102,7 @@ export const TenderDocuments = ({ tender }) => {
try {
const result = await inspectStoredDocument(b);
if (token !== getToken() || request !== onizlemeIstegi.current) return;
setMetinOnizleme({...result,name:b.ad});
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); }
};
@@ -419,6 +420,7 @@ export const TenderDocuments = ({ tender }) => {
Metni resmi dokümanla karşılaştırın; okunmuş olması doğruluğunun onaylandığı anlamına gelmez.
</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} />}
<button onClick={()=>setMetinOnizleme(null)} className="text-xs underline">Metin önizlemesini kapat</button>
</section>}
+5
View File
@@ -114,3 +114,8 @@ export const saveTenderState = (key, patch, snapshot, version) =>
export const reportDocumentReadError = (id, expectedProcessing) =>
iste('/api/me/documents/read-error', { method:'POST', body:JSON.stringify({id,expectedProcessing:expectedProcessing ?? null}) });
export const getDocumentTextReview = (id) => iste(`/api/me/documents/text-review?id=${encodeURIComponent(id)}`);
export const saveDocumentTextReview = (id, data) => iste('/api/me/documents/text-review', {
method:'POST', body:JSON.stringify({id,...data}),
});