Preserve preparation conditions and invalidate outdated source reviews

This commit is contained in:
2026-09-06 10:43:14 +03:00
parent fef6f2ab89
commit fa56e0fe83
5 changed files with 57 additions and 2 deletions
+8
View File
@@ -639,3 +639,11 @@ Doğrulama: yerel izole HTTP testinde eşzamanlı 16 oluşturma isteğinden tam
Üretim derlemesi ölçümü: ana JavaScript 1.097,39 KB'den 526,09 KB'ye (gzip 306,24 KB'den 156,35 KB'ye) düştü. Grafik ortak paketi 384,20 KB olarak ayrıldı. Bunlar dosya boyutlarıdır; gerçek ağda ilk açılış süresi/LCP ölçülmüş değildir. 940 KB ExcelJS ve 500 KB üstü ana paket uyarıları hâlâ var; performans maddesi tamamen kapanmadı. Üretim derlemesi ölçümü: ana JavaScript 1.097,39 KB'den 526,09 KB'ye (gzip 306,24 KB'den 156,35 KB'ye) düştü. Grafik ortak paketi 384,20 KB olarak ayrıldı. Bunlar dosya boyutlarıdır; gerçek ağda ilk açılış süresi/LCP ölçülmüş değildir. 940 KB ExcelJS ve 500 KB üstü ana paket uyarıları hâlâ var; performans maddesi tamamen kapanmadı.
Yerel tarayıcı kabulünde KDV, API, Belge Kasası ve Analitik ekranları açıldı; ana ihale listesine dönüşte beş sentetik ihale korundu. Kanıt qa-deferred-views.json. Geliştirme sunucusunda menü geçişi kabulüdür; üretim paketi ağ hatası/yeniden deneme ve tüm 19 ekranın etkileşim kabulü değildir. Tüm 137 test ve üretim derlemesi geçti. Canlı yayın yapılmadı. Yerel tarayıcı kabulünde KDV, API, Belge Kasası ve Analitik ekranları açıldı; ana ihale listesine dönüşte beş sentetik ihale korundu. Kanıt qa-deferred-views.json. Geliştirme sunucusunda menü geçişi kabulüdür; üretim paketi ağ hatası/yeniden deneme ve tüm 19 ekranın etkileşim kabulü değildir. Tüm 137 test ve üretim derlemesi geçti. Canlı yayın yapılmadı.
### D06 / D07 — Hazırlıkta yeterlik ve risk bilgisinin korunması
Hazırlık taslağı daha önce analizden yalnız kalem/belge listesi, kaynak sürümü ve teklif koşullarını saklıyordu. Yeterlik, kritik şart, ceza, değerlendirme dışı kalma riski, kritik ayrıntı, takvim ve maliyet yükümlülüğü gibi bilgiler yeni ortak hazırlık kaynak seçicisiyle saklanır; kaynak kanıtı, alıntılar, özet ve kapsam da korunur.
Kaynak karşılaştırma imzasına bu bilgiler dahil edildi. Aynı doküman sürümünden yeni bir kritik şart çıkarılması artık eski kaynak onayını geçerli bırakmaz. Eski kayıtlar yeni imzayla yeniden kontrol gerektirir; fiyatlar/kullanıcı düzeltmeleri silinmez. Hazırlık formunda yeterlik/risk/koşullar başlangıçta açık bir bölümde gösterilir; genel karşılaştırma beyanı bunları da kapsar. Listenin eksiksiz veya koşulların karşılanmış olduğu iddia edilmez.
Test: taslak kaynak verisinin JSON kaydetme/okuma turunda yeterlik, ceza ve kanıtı koruması; kayıt öncesi/sonrası imzanın eşitliği; sekiz koşul kategorisindeki değişiklikte eski onayın geçersizleşmesi. Tüm 138 test ve üretim derlemesi geçti. Yeni koşul bölümünün tarayıcı/gerçek dosya kabulü açık. Büyük paket uyarıları sürüyor; canlı yayın yapılmadı.
+13 -2
View File
@@ -1,3 +1,4 @@
import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
import { preparationDeadlineProblem } from '../services/preparationDeadline'; import { preparationDeadlineProblem } from '../services/preparationDeadline';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms'; import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook'; import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
@@ -116,7 +117,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
useEffect(()=>{mounted.current=true;return ()=>{mounted.current=false;printWindow.current?.close();};},[]); useEffect(()=>{mounted.current=true;return ()=>{mounted.current=false;printWindow.current?.close();};},[]);
const saveDraft = async () => { const saveDraft = async () => {
const submitted = latestDraft.current; const submitted = latestDraft.current;
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null,procurementTerms:effectiveResults.procurementTerms || null,sourceReview:effectiveResults.sourceReview || null}}}); const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,analysisSource:preparationAnalysisSource(effectiveResults)}});
if (!mounted.current) return; if (!mounted.current) return;
if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); } if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
@@ -229,10 +230,20 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<label className="flex items-start gap-2 text-xs text-slate-800"> <label className="flex items-start gap-2 text-xs text-slate-800">
<input type="checkbox" checked={!kaynakKontrolGerekli} <input type="checkbox" checked={!kaynakKontrolGerekli}
onChange={e => setReviewedSource(e.target.checked ? sourceSnapshot : null)} /> onChange={e => setReviewedSource(e.target.checked ? sourceSnapshot : null)} />
Bu sürümün kalemlerini, miktarlarını, teklif türünü ve geçerlilik süresini resmi dokümanla karşılaştırdım. Bu sürümün kalemlerini, miktarlarını, teklif türünü, geçerlilik süresini ve aşağıdaki yeterlik/risk koşullarını resmi dokümanla karşılaştırdım.
</label> </label>
</div> </div>
{preparationConditionSections.some(([field]) => effectiveResults[field]?.length > 0) &&
<details className="rounded-xl border border-amber-300 p-3" open>
<summary className="font-bold text-sm cursor-pointer">Yeterlik, risk ve diğer kaynak koşulları</summary>
<p className="text-xs mt-2">Analiz bulgularını resmi maddelerle karşılaştırın. Bu liste bütün koşulların bulunduğunu veya karşılandığını kanıtlamaz.</p>
{preparationConditionSections.filter(([field]) => effectiveResults[field]?.length > 0).map(([field,label]) =>
<section key={field} className="mt-3 text-xs"><h5 className="font-bold">{label}</h5>
<ul className="list-disc pl-5 space-y-1">{effectiveResults[field].map((row,index)=><li key={index}>{conditionText(row)}</li>)}</ul>
</section>)}
</details>}
{sourceOptions.conflict && <fieldset className="rounded-xl border border-amber-400 bg-amber-50 p-3 space-y-2"> {sourceOptions.conflict && <fieldset className="rounded-xl border border-amber-400 bg-amber-50 p-3 space-y-2">
<legend className="text-sm font-bold">Kalem kaynakları farklı</legend> <legend className="text-sm font-bold">Kalem kaynakları farklı</legend>
<p className="text-xs">İhale havuzundaki liste ile analiz listesi aynı değil. Resmi cetvelle karşılaştırıp kullanılacak listeyi seçin. Seçim uygunluk onayı değildir; ardından kalem, fiyat ve belge kontrollerini tamamlayın.</p> <p className="text-xs">İhale havuzundaki liste ile analiz listesi aynı değil. Resmi cetvelle karşılaştırıp kullanılacak listeyi seçin. Seçim uygunluk onayı değildir; ardından kalem, fiyat ve belge kontrollerini tamamlayın.</p>
+2
View File
@@ -1,3 +1,4 @@
import { preparationAnalysisSource } from './preparationAnalysis.js';
// Prices belong to a specific source row, never to its current array position. // Prices belong to a specific source row, never to its current array position.
export function sourceRows(tender, results, preferredSource = 'tender') { export function sourceRows(tender, results, preferredSource = 'tender') {
const items = preferredSource !== 'analysis' && tender?.items?.length const items = preferredSource !== 'analysis' && tender?.items?.length
@@ -33,6 +34,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res
deadline: [tender?.tarih || '', tender?.saat || ''], deadline: [tender?.tarih || '', tender?.saat || ''],
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null, documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
procurementTerms: results?.procurementTerms || null, procurementTerms: results?.procurementTerms || null,
analysis: preparationAnalysisSource(results),
humanReview: results?.sourceReview?.humanReview || null, humanReview: results?.sourceReview?.humanReview || null,
analyzedItems: results?.itemsAndSpecs || [], analyzedItems: results?.itemsAndSpecs || [],
rows: rows.map(r => r.priceKey).sort(), rows: rows.map(r => r.priceKey).sort(),
+21
View File
@@ -0,0 +1,21 @@
export const preparationConditionSections = [
['qualificationCriteria','Yeterlik koşulları'],
['criticalRequirements','Kritik şartlar'],
['risksAndPenalties','Riskler ve cezalar'],
['disqualificationRisks','Değerlendirme dışı kalma riskleri'],
['criticalDetails','Kontrol edilecek ayrıntılar'],
['deadlines','Süreler ve tarihler'],
['costDrivers','Maliyet yükümlülükleri'],
['questionsForAdministration','İdareye sorulacak sorular'],
];
export function preparationAnalysisSource(results = {}) {
const fields = ['itemsAndSpecs','documentChecklist','roadmap',...preparationConditionSections.map(([field])=>field)];
return Object.fromEntries([
...fields.map(field=>[field,results[field] || []]),
...['documentVersion','procurementTerms','sourceReview','sourceEvidence','citations','summary','scopeOfWork'].map(field=>[field,results[field] ?? null]),
]);
}
export function conditionText(row) {
if (typeof row === 'string') return row;
return ['title','label','value','clause','text','detail','where','whyItMatters','note'].map(key=>row?.[key]).filter(Boolean).join(' — ');
}
+13
View File
@@ -44,3 +44,16 @@ test('review revision changes invalidate the preparation source even with identi
const second={sourceReview:{humanReview:{documentId:'doc',revision:2,textHash:'same'}}}; const second={sourceReview:{humanReview:{documentId:'doc',revision:2,textHash:'same'}}};
assert.notEqual(bidSourceSnapshot(tender,first),bidSourceSnapshot(tender,second)); assert.notEqual(bidSourceSnapshot(tender,first),bidSourceSnapshot(tender,second));
}); });
test('persisted preparation retains critical conditions and changed findings invalidate review', async()=>{
const {preparationAnalysisSource,preparationConditionSections}=await import('../src/services/preparationAnalysis.js');
const analysis={itemsAndSpecs:[],qualificationCriteria:['Servis yetkisi'],criticalRequirements:['Numune zorunlu'],risksAndPenalties:[{text:'Gecikme cezası',clause:'Madde 12'}],sourceEvidence:[{quote:'numune'}],documentVersion:'same-document'};
const saved=JSON.parse(JSON.stringify(preparationAnalysisSource(analysis)));
assert.deepEqual(saved.qualificationCriteria,analysis.qualificationCriteria);
assert.deepEqual(saved.risksAndPenalties,analysis.risksAndPenalties);
assert.deepEqual(saved.sourceEvidence,analysis.sourceEvidence);
assert.equal(bidSourceSnapshot(tender,analysis),bidSourceSnapshot(tender,saved));
for(const [field] of preparationConditionSections) {
assert.notEqual(bidSourceSnapshot(tender,saved),bidSourceSnapshot(tender,{...saved,[field]:['Yeni koşul']}),field);
}
});