From fa56e0fe83a9f873dcd4b1d2849517d32c4893b8 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:43:14 +0300 Subject: [PATCH] Preserve preparation conditions and invalidate outdated source reviews --- docs/quality/uygulama-durumu.md | 8 ++++++++ src/components/BidPackageBuilder.jsx | 15 +++++++++++++-- src/services/bidSource.js | 2 ++ src/services/preparationAnalysis.js | 21 +++++++++++++++++++++ tests/bid-source.test.mjs | 13 +++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/services/preparationAnalysis.js diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index eb9e697..46514e0 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -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ı. 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ı. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 1fc0b50..17a0366 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -1,3 +1,4 @@ +import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis'; import { preparationDeadlineProblem } from '../services/preparationDeadline'; import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms'; 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();};},[]); const saveDraft = async () => { 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 (saved) { setDirty(latestDraft.current !== submitted); setHata(null); } else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); @@ -229,10 +230,20 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { + {preparationConditionSections.some(([field]) => effectiveResults[field]?.length > 0) && +
+ Yeterlik, risk ve diğer kaynak koşulları +

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.

+ {preparationConditionSections.filter(([field]) => effectiveResults[field]?.length > 0).map(([field,label]) => +
{label}
+
    {effectiveResults[field].map((row,index)=>
  • {conditionText(row)}
  • )}
+
)} +
} + {sourceOptions.conflict &&
Kalem kaynakları farklı

İ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.

diff --git a/src/services/bidSource.js b/src/services/bidSource.js index 1461ead..c989020 100644 --- a/src/services/bidSource.js +++ b/src/services/bidSource.js @@ -1,3 +1,4 @@ +import { preparationAnalysisSource } from './preparationAnalysis.js'; // Prices belong to a specific source row, never to its current array position. export function sourceRows(tender, results, preferredSource = 'tender') { 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 || ''], documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null, procurementTerms: results?.procurementTerms || null, + analysis: preparationAnalysisSource(results), humanReview: results?.sourceReview?.humanReview || null, analyzedItems: results?.itemsAndSpecs || [], rows: rows.map(r => r.priceKey).sort(), diff --git a/src/services/preparationAnalysis.js b/src/services/preparationAnalysis.js new file mode 100644 index 0000000..4762848 --- /dev/null +++ b/src/services/preparationAnalysis.js @@ -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(' — '); +} diff --git a/tests/bid-source.test.mjs b/tests/bid-source.test.mjs index 680e4bf..a7524db 100644 --- a/tests/bid-source.test.mjs +++ b/tests/bid-source.test.mjs @@ -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'}}}; 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); + } +});