Preserve preparation conditions and invalidate outdated source reviews
This commit is contained in:
@@ -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ı.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(' — ');
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user