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
+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}),
});