fix: wrap long bid items within printable page width

This commit is contained in:
2026-09-06 15:12:10 +03:00
parent fb2c2b120e
commit 4d6d7009ca
4 changed files with 28 additions and 3 deletions
+9
View File
@@ -1174,3 +1174,12 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- 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`. - 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. - 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. - 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.
### 2026-09-06 — 100 kalemli cetvelde gerçek yatay taşma düzeltmesi
- Üretim HTML/CSS çıktısını doğrudan sunan `/qa-print-cetvel` ve 100 satırlı sentetik örnek eklendi. Örnek Türkçe karakterler, uzun açıklamalar, 144 karakterlik boşluksuz ürün kodu ve üç ondalıklı birim fiyat içeriyor.
- Tarayıcıda başlangıçta 1280 px görünümün kaydırma genişliği 1637 px, tablo genişliği 1629 px idi. Boşluksuz ürün kodu ad sütununu büyütüp fiyat sütunlarını görünüm dışına taşıyordu. Cetvel sabit sütun düzeni ve hücre içi kırılma kullanıyor; ekran önizlemesi A4 üzerindeki 174 mm içerik genişliğiyle sınırlandı. Yazdırmada body kenar boşluğu sıfırlanıp @page kenar boşlukları korunuyor.
- Genel toplamın tekrar eden sayfa altlığı olmaması için tfoot normal satır grubu yapıldı; imza bloğuna bölünmeme kuralı eklendi. Bu iki kuralın hesaplanan CSS değerleri doğrulandı; gerçek yazdırma sayfa geçişi ayrıca kabul gerektiriyor.
- Son tarayıcı ölçümü: 657,63 px içerik/tablo genişliği, sıfır taşan hücre, 100 sıralı kalem. Her satırda 3 adet, 125,335 TL birim fiyat ve 376,01 TL tutar; genel toplam 37.601,00 TL. Türkçe metin ve uzun kod bütünüyle korundu. Kanıt `rapor/qa-print-layout.json` ve ekran görüntüsü `rapor/qa-print-layout.png`.
- 275 test ve build başarılı. Desktop Commander ile denenen PDF dönüşümü üretim stilini değiştirdi ve yazdırma düğmesini PDF içine bastı; `qa-100-kalem-before.pdf` üretim çıktısının doğrulama kanıtı değildir. Bu deneme üzerinden üretim sayfalama hatası iddia edilmedi. Doğrulanan kapsam gerçek HTML görünümünde A4 genişliği/taşma/içerik kontrolüdür. Tarayıcıdan kaydedilmiş çok sayfalı PDF ve beş resmi dosya kabulü açık; genel plan tamamlanmadı.
+7 -3
View File
@@ -62,7 +62,7 @@ const kacir = (s) =>
const STIL = ` const STIL = `
@page { size: A4; margin: 20mm 18mm; } @page { size: A4; margin: 20mm 18mm; }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { font-family: "Times New Roman", Georgia, serif; font-size: 11.5pt; line-height: 1.55; color: #000; } body { margin: 0; font-family: "Times New Roman", Georgia, serif; font-size: 11.5pt; line-height: 1.55; color: #000; overflow-wrap: anywhere; }
h1 { font-size: 13pt; text-align: center; margin: 0 0 4mm; letter-spacing: .3px; } h1 { font-size: 13pt; text-align: center; margin: 0 0 4mm; letter-spacing: .3px; }
h2 { font-size: 11pt; margin: 6mm 0 2mm; } h2 { font-size: 11pt; margin: 6mm 0 2mm; }
.ust { font-size: 10pt; margin-bottom: 6mm; } .ust { font-size: 10pt; margin-bottom: 6mm; }
@@ -70,16 +70,20 @@ const STIL = `
table { width: 100%; border-collapse: collapse; margin: 3mm 0; font-size: 9.5pt; } table { width: 100%; border-collapse: collapse; margin: 3mm 0; font-size: 9.5pt; }
th, td { border: 1px solid #444; padding: 1.6mm 2mm; text-align: left; vertical-align: top; } th, td { border: 1px solid #444; padding: 1.6mm 2mm; text-align: left; vertical-align: top; }
thead { display: table-header-group; } thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr { break-inside: avoid; } tr { break-inside: avoid; }
th { background: #eee; font-weight: bold; } th { background: #eee; font-weight: bold; }
td.s, th.s { text-align: right; white-space: nowrap; } td.s, th.s { text-align: right; white-space: nowrap; }
.cetvel { table-layout: fixed; }
.cetvel td, .cetvel th { white-space: normal; overflow-wrap: anywhere; }
.toplam td { font-weight: bold; background: #f4f4f4; } .toplam td { font-weight: bold; background: #f4f4f4; }
.imza { margin-top: 12mm; width: 70mm; margin-left: auto; text-align: center; font-size: 10pt; } .imza { break-inside: avoid; margin-top: 12mm; width: 70mm; margin-left: auto; text-align: center; font-size: 10pt; }
.imza .kutu { border: 1px solid #666; height: 26mm; margin-bottom: 2mm; } .imza .kutu { border: 1px solid #666; height: 26mm; margin-bottom: 2mm; }
.not { font-size: 8.5pt; color: #555; margin-top: 8mm; } .not { font-size: 8.5pt; color: #555; margin-top: 8mm; }
ul { margin: 2mm 0; padding-left: 6mm; } ul { margin: 2mm 0; padding-left: 6mm; }
li { margin: 1.2mm 0; } li { margin: 1.2mm 0; }
.kutucuk { display: inline-block; width: 3.6mm; height: 3.6mm; border: 1px solid #333; margin-right: 2mm; vertical-align: -0.4mm; } .kutucuk { display: inline-block; width: 3.6mm; height: 3.6mm; border: 1px solid #333; margin-right: 2mm; vertical-align: -0.4mm; }
@media screen { body { width: calc(100% - 32px); max-width: 174mm; margin: 20mm auto; } }
@media print { .yazdirma { display: none; } } @media print { .yazdirma { display: none; } }
`; `;
@@ -184,7 +188,7 @@ export function teklifCetveliHtml(f, satirlar, toplam) {
return `<h1>BİRİM FİYAT TEKLİF CETVELİ</h1> return `<h1>BİRİM FİYAT TEKLİF CETVELİ</h1>
${ustBilgi(f)} ${ustBilgi(f)}
<table> <table class="cetvel">
<thead><tr> <thead><tr>
<th class="s" style="width:8%">Sıra</th><th>Mal / hizmet adı</th> <th class="s" style="width:8%">Sıra</th><th>Mal / hizmet adı</th>
<th class="s" style="width:12%">Miktar</th><th style="width:12%">Birim</th> <th class="s" style="width:12%">Miktar</th><th style="width:12%">Birim</th>
+2
View File
@@ -7,6 +7,7 @@ import {join} from 'node:path';
import {randomBytes} from 'node:crypto'; import {randomBytes} from 'node:crypto';
import {createServer as netServer} from 'node:net'; import {createServer as netServer} from 'node:net';
import {readyBid} from './fixtures/ready-bid.mjs'; import {readyBid} from './fixtures/ready-bid.mjs';
import {printBidFixture} from './fixtures/print-bid.mjs';
import {preparationSummary} from '../src/services/preparationSummary.js'; import {preparationSummary} from '../src/services/preparationSummary.js';
const dir=mkdtempSync(join(tmpdir(),'ihaleist-browser-')); const dir=mkdtempSync(join(tmpdir(),'ihaleist-browser-'));
process.env.AUTH_SECRET=randomBytes(32).toString('hex'); process.env.AUTH_SECRET=randomBytes(32).toString('hex');
@@ -53,6 +54,7 @@ const qaTelegramStore=await import('../server/user-store.mjs');
const {createTelegramLinker}=await import('../server/telegram-link.mjs'); const {createTelegramLinker}=await import('../server/telegram-link.mjs');
const qaTelegramLinker=createTelegramLinker(qaTelegramStore,()=>Date.now()+telegramClockOffset); const qaTelegramLinker=createTelegramLinker(qaTelegramStore,()=>Date.now()+telegramClockOffset);
const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use(async(req,res,next)=>{ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use(async(req,res,next)=>{
if(req.url==='/qa-print-cetvel'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(printBidFixture().html);return;}
if(req.url==='/qa-responsive'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel ekran boyutu testi</h1><p>Gerçek uygulama ayrı bir görünüm alanında yüklenir.</p><button data-width="390">390 piksel</button><button data-width="768">768 piksel</button><button data-width="1440">1440 piksel</button><pre id="metrics" aria-label="Görünüm ölçümleri"></pre><iframe title="İhaleist test görünümü" src="/" style="display:block;width:390px;height:900px;border:0"></iframe><script>document.querySelectorAll('button').forEach(button=>button.onclick=()=>document.querySelector('iframe').style.width=button.dataset.width+'px');setInterval(()=>{const doc=document.querySelector('iframe').contentDocument;if(!doc?.body)return;const width=doc.documentElement.clientWidth;const groups=[...doc.querySelectorAll('nav[aria-label="Çalışma alanları"] button')].map(el=>({label:el.textContent.trim(),left:Math.round(el.getBoundingClientRect().left),right:Math.round(el.getBoundingClientRect().right)}));document.getElementById('metrics').textContent=JSON.stringify({width,scrollWidth:doc.documentElement.scrollWidth,groups},null,2);},400);</script></html>`);return;} if(req.url==='/qa-responsive'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel ekran boyutu testi</h1><p>Gerçek uygulama ayrı bir görünüm alanında yüklenir.</p><button data-width="390">390 piksel</button><button data-width="768">768 piksel</button><button data-width="1440">1440 piksel</button><pre id="metrics" aria-label="Görünüm ölçümleri"></pre><iframe title="İhaleist test görünümü" src="/" style="display:block;width:390px;height:900px;border:0"></iframe><script>document.querySelectorAll('button').forEach(button=>button.onclick=()=>document.querySelector('iframe').style.width=button.dataset.width+'px');setInterval(()=>{const doc=document.querySelector('iframe').contentDocument;if(!doc?.body)return;const width=doc.documentElement.clientWidth;const groups=[...doc.querySelectorAll('nav[aria-label="Çalışma alanları"] button')].map(el=>({label:el.textContent.trim(),left:Math.round(el.getBoundingClientRect().left),right:Math.round(el.getBoundingClientRect().right)}));document.getElementById('metrics').textContent=JSON.stringify({width,scrollWidth:doc.documentElement.scrollWidth,groups},null,2);},400);</script></html>`);return;}
if(req.url==='/qa-telegram-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel Telegram testi</h1><p>Gerçek Telegram mesajı gönderilmez.</p><button id="recover">Telegram durum hatasını kapat</button><button id="enable">Sentetik bağlamayı aç</button><button id="receive">Son kodu sentetik sohbete ulaştır</button><button id="expire">Kod süresini bitir</button><p role="status"></p><script>for(const mode of ['recover','enable','receive','expire'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-telegram-fixture?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;} if(req.url==='/qa-telegram-controls'){res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel Telegram testi</h1><p>Gerçek Telegram mesajı gönderilmez.</p><button id="recover">Telegram durum hatasını kapat</button><button id="enable">Sentetik bağlamayı aç</button><button id="receive">Son kodu sentetik sohbete ulaştır</button><button id="expire">Kod süresini bitir</button><p role="status"></p><script>for(const mode of ['recover','enable','receive','expire'])document.getElementById(mode).onclick=async()=>{const r=await fetch('/qa-telegram-fixture?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;}
if(req.url?.startsWith('/qa-telegram-fixture?')&&req.method==='POST'){ if(req.url?.startsWith('/qa-telegram-fixture?')&&req.method==='POST'){
+10
View File
@@ -0,0 +1,10 @@
import {belgeYazdir,teklifCetveliHtml} from '../../src/services/bidDocs.js';
import {bidTotal} from '../../src/services/bidValidation.js';
export function printBidFixture() {
const rows=Array.from({length:100},(_,i)=>({name:`Kalem ${String(i+1).padStart(3,'0')} — Ölçüm, İğdır, Şırınga, Çözelti, tüp ve özgül laboratuvar test kiti ${i%7===0?'uzun teknik ürün açıklaması '.repeat(5):''}${i===49?'UZUNÜRÜNKODU'.repeat(12):''}`,quantity:'3 adet',birimFiyat:'125.335'}));
const form={idare:'QA İğdır Üniversitesi Sağlık Araştırma ve Uygulama Merkezi',isinAdi:'Sentetik 100 kalem Türkçe laboratuvar teklif cetveli',ikn:'QA/100',ihaleTarihi:'22.09.2026',firmaUnvan:'QA Ölçüm ve Sağlık Ürünleri',yetkiliAd:'QA Şule Çığ',yetkiliGorev:'Test Yetkilisi'};
let html='';
belgeYazdir('QA — 100 Kalem',teklifCetveliHtml(form,rows,bidTotal(rows)),{document:{open(){},write(value){html=value;},close(){}},focus(){}});
return {html,rows,total:bidTotal(rows)};
}