fix: preserve bid drafts and align tender source data

This commit is contained in:
2026-09-06 08:40:10 +03:00
parent c91631d99f
commit 456728b4bd
26 changed files with 510 additions and 81 deletions
+7 -7
View File
@@ -1,3 +1,4 @@
import { quantityParts, bidRowTotal, validateBidRows } from './bidValidation.js';
/**
* İhale dosyası belgeleri — teklif mektubu, birim fiyat teklif cetveli ve
* evrak kontrol listesi.
@@ -53,12 +54,7 @@ export const paraTR = (n) =>
(Number(n) || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
/** "3 adet" → { miktar: 3, birim: "adet" } ; okunamazsa miktar null */
export function miktarAyristir(q) {
const m = /(-?[\d.,]+)\s*(.*)$/.exec(String(q || '').trim());
if (!m) return { miktar: null, birim: String(q || '').trim() };
const sayi = parseFloat(m[1].replace(/\./g, '').replace(',', '.'));
return { miktar: isFinite(sayi) ? sayi : null, birim: (m[2] || '').trim() };
}
export const miktarAyristir = quantityParts;
const kacir = (s) =>
String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
@@ -73,6 +69,8 @@ const STIL = `
.ust div { margin: .6mm 0; }
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; }
thead { display: table-header-group; }
tr { break-inside: avoid; }
th { background: #eee; font-weight: bold; }
td.s, th.s { text-align: right; white-space: nowrap; }
.toplam td { font-weight: bold; background: #f4f4f4; }
@@ -160,10 +158,12 @@ ihale dokümanındaki standart form metniyle karşılaştırınız.</p>`;
/** Birim fiyat teklif cetveli. */
export function teklifCetveliHtml(f, satirlar, toplam) {
const errors = validateBidRows(satirlar);
if (errors.length) throw new Error(errors.join(" "));
const govde = satirlar.map((r, i) => {
const { miktar, birim } = miktarAyristir(r.quantity);
const bf = Number(r.birimFiyat) || 0;
const tutar = (miktar ?? 0) * bf;
const tutar = bidRowTotal(r);
return `<tr>
<td class="s">${i + 1}</td>
<td>${kacir(r.name)}</td>
+24
View File
@@ -0,0 +1,24 @@
export function quantityParts(value) {
if (typeof value === 'number') return {miktar:Number.isFinite(value) ? value : null,birim:''};
const match = /^\s*([+-]?[\d]+(?:[.,]\d+)*)\s+(.+?)\s*$/.exec(String(value || ''));
if (!match) return {miktar:null,birim:''};
const raw = match[1];
const normalized = raw.includes(',') ? raw.replace(/\./g,'').replace(',','.')
: /^\d{1,3}(?:\.\d{3})+$/.test(raw) ? raw.replace(/\./g,'') : raw;
const number = Number(normalized);
return {miktar:Number.isFinite(number) ? number : null,birim:match[2]};
}
export const roundMoney = n => Math.round((n + Number.EPSILON) * 100) / 100;
export function bidRowTotal(row) {
const {miktar} = quantityParts(row.quantity);
const price = Number(row.birimFiyat);
return miktar > 0 && Number.isFinite(price) && price > 0 ? roundMoney(miktar * price) : null;
}
export function validateBidRows(rows) {
if (!rows.length) return ['Resmi teklif cetvelindeki kalemler eksik.'];
return rows.flatMap((row,i) => {
const {miktar,birim} = quantityParts(row.quantity);
return (!row.name?.trim() || !(miktar > 0) || !birim || bidRowTotal(row) === null)
? [`${i+1}. kalemin adı, miktarı, birimi veya fiyatı eksik/geçersiz.`] : [];
});
}