From fb2c2b120e3ce195a3073da10cceefd2ebf81f9c Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 15:08:56 +0300 Subject: [PATCH] fix: withhold print action until bid validation finishes --- docs/quality/uygulama-durumu.md | 8 ++++++++ src/components/BidPackageBuilder.jsx | 2 +- src/services/bidDocs.js | 8 ++++---- tests/bid-print-window.test.mjs | 19 +++++++++++++++++++ tests/browser-qa.mjs | 22 +++++++++++++++++++--- tests/fixtures/ready-bid.mjs | 21 +++++++++++++++++++++ 6 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 tests/bid-print-window.test.mjs create mode 100644 tests/fixtures/ready-bid.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index a116a9c..e5f7a5d 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -1166,3 +1166,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Altı tarayıcı kontrolü geçti: başlangıç kaynak onayı, değişen ID üzerinden kazananın açık dosyaya yansıması, önceki onayın geçersiz olması, 125,50 TL birim fiyat / 251,00 TL satır toplamının korunması, çıktı düğmesinin kapalı kalması ve yeni kayıt üzerinden taslak kaydının sunucu onayı. Kanıt `rapor/qa-open-bid-source-update.json`. - Bu düzenekte firma/belge eksikleri de vardı; kapalı çıktı düğmesi tek başına kazanan kontrolünü kanıtlamaz. Kazanan uyarısının görünmesi ve kaynak onayının değişmesi doğrudan doğrulandı. Henüz başlayan PDF/Excel üretimi sırasında kaynak değişimi veya tam hazırdan engelliye geçiş kabulü yapılmadı. - Yalnız QA düzeneği değişti; önceki 274 test ve build sonucu geçerli, tekrarlanmadı. Test sekmesi ve bu çalışmanın geçici sunucusu kapatıldı. İlk düzeneğin ReactDOM import hatası test sırasında düzeltildi. Genel plan ve resmi beş dosya kabulü açık. + + +### 2026-09-06 — Hazır dosyada asenkron çıktı ve kaynak değişimi + +- Tam hazırlanmış sentetik taslak için QA düzeneği eklendi. İsteğe bağlı tek belge uygulanmıyor gerekçesiyle işaretli; firma, fiyat, kaynak ve ürün kontrolleri dolu. Bu veri resmi başvuru uygunluğu kanıtı değildir. Düzenek açılırken ortak hazırlık özeti sıfır eksik koşulunu doğruluyor. +- Görünür kontrollerle belge GET yanıtı bekletildi. Başlangıçta kaynak onayı işaretli ve cetvel çıktı düğmesi etkinken üretim başlatıldı; aynı İKN için farklı ID/başlık/kazanan yayımlandıktan sonra yanıt bırakıldı. Üretim “Form, kaynak veya oturum değişti” mesajıyla kesildi, kaynak onayı kalktı, çıktı düğmesi kapandı ve 251,00 TL toplam korundu. Altı kontrolün tamamı geçti: `rapor/qa-ready-bid-source-race.json`. +- Belge kontrol penceresindeki erken Yazdır/PDF düğmesi kaldırıldı; düğme yalnız hazırlanmış belge pencereye yazıldığında ekleniyor. Yardımcı işlevin yanlış otomatik yazdırma açıklaması düzeltildi. Regresyon testi bekleme ekranında yazdırma eyleminin olmadığını, aynı pencerenin hazır belgeyle değiştiğini ve kapatılan pencerenin reddedildiğini doğruluyor. +- 275 test ve üretim derlemesi başarılı. Mevcut büyük paket uyarısı devam ediyor. Tarayıcı sekme aracı çıktı penceresini listelemedi; pencerenin görsel içeriği ve diskte dosya oluşmaması doğrudan gözlenmedi. Doğrulanan kabul, hazır dosyanın belge kontrolünden dönüşte kaynak değişimini yakalayıp üretim hata dalına geçmesidir. Çok sayfalı PDF görsel kabulü ve gerçek beş ihale dosyası hâlâ açık. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 5a2f137..8049e36 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -180,7 +180,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu if(initialDeadlineProblem)throw new Error(initialDeadlineProblem); if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.'); // Open during the click; async validation must not lose popup activation. - printWindow.current = belgeYazdir('Dosya kontrolü', '

Belgeler kontrol ediliyor…

'); + printWindow.current = belgeYazdir('Dosya kontrolü', '

Belgeler kontrol ediliyor…

', undefined, {pending:true}); setPrinting(true); const linkedPortfolio=lotSelection.rows.some(row=>checks.products?.[row.priceKey]?.portfolioId); const readPortfolio=async()=>linkedPortfolio ? (await getAllUserData()).firma_profil?.scrapedCapabilities?.healthPortfolio || [] : []; diff --git a/src/services/bidDocs.js b/src/services/bidDocs.js index ad01456..8d805a6 100644 --- a/src/services/bidDocs.js +++ b/src/services/bidDocs.js @@ -83,19 +83,19 @@ const STIL = ` @media print { .yazdirma { display: none; } } `; -/** Belgeyi kendi penceresinde açıp yazdırma diyaloğunu tetikler. */ -export function belgeYazdir(baslik, govde, existingWindow) { +/** Belgeyi kendi penceresinde açar; kullanıcı hazır belgeyi yazdırabilir. */ +export function belgeYazdir(baslik, govde, existingWindow, {pending = false} = {}) { const w = existingWindow || window.open('', '_blank', 'width=900,height=1000'); if (!w) throw new Error('Tarayıcı yeni pencereyi engelledi. Açılır pencere iznini verip tekrar deneyin.'); if(w.closed) throw new Error('Çıktı penceresi kapatıldı. Yeniden deneyin.'); w.document.open(); w.document.write(` ${kacir(baslik)}${govde} -
+ ${pending ? '' : `
-
`); +
`}`); w.document.close(); w.focus(); return w; diff --git a/tests/bid-print-window.test.mjs b/tests/bid-print-window.test.mjs new file mode 100644 index 0000000..f442ea8 --- /dev/null +++ b/tests/bid-print-window.test.mjs @@ -0,0 +1,19 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {belgeYazdir} from '../src/services/bidDocs.js'; + +test('validation window has no print action until replaced by the prepared document',()=>{ + let html='',focused=0; + const popup={closed:false,document:{open(){html='';},write(value){html+=value;},close(){}},focus(){focused++;}}; + assert.equal(belgeYazdir('Dosya kontrolü','

Belgeler kontrol ediliyor…

',popup,{pending:true}),popup); + assert.match(html,/Belgeler kontrol ediliyor/); + assert.doesNotMatch(html,/','

Hazır cetvel

',popup),popup); + assert.match(html,/Teklif <Cetveli><\/title>/); + assert.match(html,/Hazır cetvel/); + assert.match(html,/<button onclick="window\.print\(\)"/); + assert.doesNotMatch(html,/Belgeler kontrol ediliyor/); + assert.equal(focused,2); + popup.closed=true; + assert.throws(()=>belgeYazdir('Teklif','',popup),/penceresi kapatıldı/); +}); diff --git a/tests/browser-qa.mjs b/tests/browser-qa.mjs index 6ff53d6..d4ac44c 100644 --- a/tests/browser-qa.mjs +++ b/tests/browser-qa.mjs @@ -6,6 +6,8 @@ import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {randomBytes} from 'node:crypto'; import {createServer as netServer} from 'node:net'; +import {readyBid} from './fixtures/ready-bid.mjs'; +import {preparationSummary} from '../src/services/preparationSummary.js'; const dir=mkdtempSync(join(tmpdir(),'ihaleist-browser-')); process.env.AUTH_SECRET=randomBytes(32).toString('hex'); const {signToken}=await import('../server/auth.mjs'); @@ -15,6 +17,8 @@ const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true}; const keyToken=signToken(keyUser); let apiKeyListFailure=false; let briefFailure='none'; +let holdDocuments=false; +const pendingDocuments=new Set(); process.env.IHALEIST_USER_DATA_DIR=dir; const {createBriefOutbox}=await import('../server/brief-outbox.mjs'); const qaBriefOutbox=createBriefOutbox(dir); @@ -96,17 +100,29 @@ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{targ res.setHeader('Content-Type','application/json');res.end(JSON.stringify({success:true,job:qaJobs.getAiJob(user.email,created.id)})); }catch(error){res.writeHead(400,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:error.message}));}});return; } - if(req.url==='/qa-source-update') { + if(req.url?.startsWith('/qa-output-documents?')&&req.method==='POST') { + const mode=new URL(req.url,'http://localhost').searchParams.get('mode'); + if(mode==='hold')holdDocuments=true; + if(mode==='release'){holdDocuments=false;for(const pending of pendingDocuments){pending.writeHead(200,{'Content-Type':'application/json'});pending.end(JSON.stringify({success:true,belgeler:[]}));}pendingDocuments.clear();} + res.end('Belge yanıtı: '+mode);return; + } + if(req.url==='/api/me/documents'&&req.method==='GET'&&holdDocuments){pendingDocuments.add(res);res.on('close',()=>pendingDocuments.delete(res));return;} + if(req.url==='/qa-source-update'||req.url==='/qa-source-update?ready=1') { + const selected=req.url.endsWith('?ready=1')?readyBid(tenders[0]):tenders[0]; + if(selected.bidDraft){const summary=preparationSummary(selected,{loading:false,error:null,documents:[]});if(summary.issues.length)throw new Error('QA ready fixture: '+JSON.stringify(summary.issues));} const html=`<html lang="tr"><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><div id="root"></div><script type="module"> import React from '/node_modules/.vite/deps/react.js'; import ReactDOM from '/node_modules/.vite/deps/react-dom_client.js'; import {AppProvider,useApp} from '/src/context/AppContext.jsx'; import {BidPackageBuilder} from '/src/components/BidPackageBuilder.jsx'; import '/src/index.css'; - const selected=${JSON.stringify(tenders[0])}; - function Probe(){const {setTenders}=useApp();return React.createElement('main',{}, + const selected=${JSON.stringify(selected)}; + function Probe(){const {setTenders}=useApp();const [status,setStatus]=React.useState('');const control=async mode=>setStatus(await (await fetch('/qa-output-documents?mode='+mode,{method:'POST'})).text());return React.createElement('main',{}, React.createElement('h1',{},'Yerel kaynak güncelleme testi'), React.createElement('p',{},'Yalnız sentetik veri. Açık dosya korunurken güncel kaynak değişir.'), + React.createElement('button',{onClick:()=>control('hold')},'Belge yanıtını beklet'), + React.createElement('button',{onClick:()=>control('release')},'Belge yanıtını serbest bırak'), + React.createElement('p',{role:'status'},status), React.createElement('button',{onClick:()=>setTenders([{...selected,id:'qa-rotated-source',baslik:'QA Güncellenmiş alım',kazananFirma:'QA Kazanan'}])},'Aynı İKN için yeni kayıt ID ve kazanan yayımla'), React.createElement(BidPackageBuilder,{tender:selected}));} ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(AppProvider,{},React.createElement(Probe))); diff --git a/tests/fixtures/ready-bid.mjs b/tests/fixtures/ready-bid.mjs new file mode 100644 index 0000000..4f97e06 --- /dev/null +++ b/tests/fixtures/ready-bid.mjs @@ -0,0 +1,21 @@ +// Synthetic UI fixture, never a claim of official tender eligibility. +import {sourceRows,bidSourceOptions,bidSourceSnapshot} from '../../src/services/bidSource.js'; +import {documentKey,HEALTH_CHECKS} from '../../src/services/preparationChecks.js'; + +export function readyBid(tender) { + const document={document:'QA isteğe bağlı belge',mandatory:false,note:'Yalnız sentetik senaryo'}; + const analysis={documentChecklist:[document]},itemSource={}; + const partialBid={enabled:false,source:'',selected:[],assignments:{}}; + const options=bidSourceOptions(tender,analysis,itemSource); + const rows=sourceRows(tender,analysis,options.selected || 'tender'); + const sourceSnapshot=JSON.stringify([bidSourceSnapshot(tender,analysis,rows),partialBid,itemSource,options.signature]); + return {...tender,bidDraft:{ + firma:{firmaUnvan:'QA Sentetik Laboratuvar',adres:'QA Test Adresi',vergiDairesi:'QA',vergiNo:'0000000000',yetkiliAd:'QA Test Kullanıcısı',yetkiliGorev:'Test',telefon:'0000000000',eposta:'qa@example.test'}, + teklifTuru:'birim',gecerlilikGun:'120',kisim:'',goturuBedel:'',priceSchema:1, + pricesBySource:Object.fromEntries(rows.map(row=>[row.priceKey,'125.50'])), + sourceSnapshot,reviewedSource:sourceSnapshot,manualDocuments:[],partialBid,itemSource,analysisSource:analysis, + checks:{documents:{[documentKey(document)]:{status:'notApplicable',evidence:'QA senaryosunda istenmiyor.'}}, + products:Object.fromEntries(rows.map(row=>[row.priceKey,{status:'suitable',product:'QA sentetik ürün',source:'QA teknik madde 1', + ...Object.fromEntries(HEALTH_CHECKS.map(([key])=>[key,{status:'notApplicable',evidence:'QA senaryosu.'}]))}]))}, + }}; +}