fix: preserve Turkish decimal prices and verify preparation persistence

This commit is contained in:
2026-09-06 08:50:18 +03:00
parent b94ab97241
commit 311344306b
8 changed files with 387 additions and 10 deletions
@@ -0,0 +1,150 @@
- status: Sunucuda kayıtlı taslak
- alert: Kalemler ve teklif koşulları resmi dokümanla henüz karşılaştırılmadı. Kaynağı kontrol etmeden teklif çıktısı oluşturulamaz.
- checkbox "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."
- text: 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.
- heading "İstekli (firma) bilgileri" [level=5]
- generic: Firma ticaret unvanı *
- textbox "Firma ticaret unvanı *":
- /placeholder: ""
- text: QA Laboratuvar Test Ltd
- generic: Tebligata esas adres *
- textbox "Tebligata esas adres *":
- /placeholder: ""
- text: Yerel test adresi
- generic: Vergi dairesi *
- textbox "Vergi dairesi *":
- /placeholder: ""
- text: Test
- generic: Vergi numarası *
- textbox "Vergi numarası *":
- /placeholder: ""
- text: "0000000000"
- generic: Ticaret sicil / MERSİS no
- textbox "Ticaret sicil / MERSİS no":
- /placeholder: ""
- generic: Yetkili ad soyad *
- textbox "Yetkili ad soyad *":
- /placeholder: ""
- text: Test Yetkilisi
- generic: Yetkilinin görevi *
- textbox "Yetkilinin görevi *":
- /placeholder: Örn. Genel Müdür
- text: Test Sorumlusu
- generic: Telefon *
- textbox "Telefon *":
- /placeholder: ""
- text: "0000000000"
- generic: E-posta *
- textbox "E-posta *":
- /placeholder: ""
- text: qa@example.test
- generic: Teklif türü
- combobox "Teklif türü":
- option "Birim fiyat" [selected]
- option "Anahtar teslim götürü bedel"
- generic: Teklif geçerlilik (gün)
- textbox "Teklif geçerlilik (gün)": "120"
- generic: Kısım (varsa)
- textbox "Kısım (varsa)":
- /placeholder: Örn. 2. kısım
- heading "Kalem fiyatlandırma" [level=5]
- paragraph: Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.
- table:
- rowgroup:
- row "Kalem Miktar Birim fiyat Tutar":
- columnheader "Kalem"
- columnheader "Miktar"
- columnheader "Birim fiyat"
- columnheader "Tutar"
- rowgroup:
- row "Sentetik test kalemi 2 adet 125,50 251,00":
- cell "Sentetik test kalemi"
- cell "2 adet"
- cell "125,50":
- textbox "0": 125,50
- cell "251,00"
- region "Belge ve ürün uygunluğu":
- heading "Belge ve ürün uygunluğu" [level=5]
- paragraph: Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.
- group "Sentetik yetki belgesi (zorunlu)":
- generic: Sentetik yetki belgesi (zorunlu)
- paragraph: Yerel QA dokümanı madde 7
- button "Elle eklenen gereksinimi kaldır"
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı" [selected]
- text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
- textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde": QA dosyası v1 — yalnız test, madde 7
- paragraph: Resmi dokümandan eksik belge gereksinimi ekle
- text: Belge adı
- textbox "Belge adı"
- text: Kaynak doküman ve madde
- textbox "Kaynak doküman ve madde"
- button "Belge gereksinimi ekle" [disabled]
- generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü" [active]
- text: Teklif edilen marka/model veya hizmet kapsamı
- textbox "Teklif edilen marka/model veya hizmet kapsamı": QA Model 1
- text: Teknik doküman/madde ve uygunluk dayanağı
- textbox "Teknik doküman/madde ve uygunluk dayanağı": Sentetik QA teknik madde 3
- text: Ürün/hizmet uygunluğu
- combobox "Ürün/hizmet uygunluğu":
- option "Kontrol gerekli"
- option "Uygun" [selected]
- option "Uygun değil"
- group "ÜTS / ürün kaydı":
- generic: ÜTS / ürün kaydı
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi
- textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir.
- group "Numune / demonstrasyon":
- generic: Numune / demonstrasyon
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi
- textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir.
- group "Kurulum / bakım / servis":
- generic: Kurulum / bakım / servis
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi
- textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir.
- group "Teslimat ve süre":
- generic: Teslimat ve süre
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi
- textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir.
- text: Sorumlu
- textbox "Sorumlu": QA Yetkilisi
- generic: Teklif toplamı (KDV hariç)
- generic: 251,00 TL
- paragraph: "Yazı ile: ikiyüzellibir Türk Lirası"
- heading "Hazırlanacak belgeler" [level=5]
- button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]:
- generic: Teklif Mektubu
- generic: Rakam ve yazı ile bedel
- text: Yazdır / PDF
- button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]:
- generic: Birim Fiyat Teklif Cetveli
- generic: 1 kalem
- text: Yazdır / PDF
- button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]:
- generic: Evrak Kontrol Listesi
- generic: 1 belge
- text: Yazdır / PDF
- paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.
- paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın.
- button
- button "Kullanım kılavuzu (? tuşu)":
- region "Notifications (F8)":
- list
@@ -0,0 +1,122 @@
- status: Sunucuda kayıtlı taslak
- alert: Kalemler ve teklif koşulları resmi dokümanla henüz karşılaştırılmadı. Kaynağı kontrol etmeden teklif çıktısı oluşturulamaz.
- checkbox "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."
- text: 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.
- heading "İstekli (firma) bilgileri" [level=5]
- generic: Firma ticaret unvanı *
- textbox "Firma ticaret unvanı *":
- /placeholder: ""
- text: QA Laboratuvar Test Ltd
- generic: Tebligata esas adres *
- textbox "Tebligata esas adres *":
- /placeholder: ""
- text: Yerel test adresi
- generic: Vergi dairesi *
- textbox "Vergi dairesi *":
- /placeholder: ""
- text: Test
- generic: Vergi numarası *
- textbox "Vergi numarası *":
- /placeholder: ""
- text: "0000000000"
- generic: Ticaret sicil / MERSİS no
- textbox "Ticaret sicil / MERSİS no":
- /placeholder: ""
- generic: Yetkili ad soyad *
- textbox "Yetkili ad soyad *":
- /placeholder: ""
- text: Test Yetkilisi
- generic: Yetkilinin görevi *
- textbox "Yetkilinin görevi *":
- /placeholder: Örn. Genel Müdür
- text: Test Sorumlusu
- generic: Telefon *
- textbox "Telefon *":
- /placeholder: ""
- text: "0000000000"
- generic: E-posta *
- textbox "E-posta *":
- /placeholder: ""
- text: qa@example.test
- generic: Teklif türü
- combobox "Teklif türü":
- option "Birim fiyat" [selected]
- option "Anahtar teslim götürü bedel"
- generic: Teklif geçerlilik (gün)
- textbox "Teklif geçerlilik (gün)": "120"
- generic: Kısım (varsa)
- textbox "Kısım (varsa)":
- /placeholder: Örn. 2. kısım
- heading "Kalem fiyatlandırma" [level=5]
- table:
- rowgroup:
- row "Kalem Miktar Birim fiyat Tutar":
- columnheader "Kalem"
- columnheader "Miktar"
- columnheader "Birim fiyat"
- columnheader "Tutar"
- rowgroup:
- row "Sentetik test kalemi 2 adet 125.50 251,00":
- cell "Sentetik test kalemi"
- cell "2 adet"
- cell "125.50":
- textbox "0": "125.50"
- cell "251,00"
- region "Belge ve ürün uygunluğu":
- heading "Belge ve ürün uygunluğu" [level=5]
- paragraph: Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.
- group "Sentetik yetki belgesi (zorunlu)":
- generic: Sentetik yetki belgesi (zorunlu)
- paragraph: Yerel QA dokümanı madde 7
- button "Elle eklenen gereksinimi kaldır"
- combobox "Kontrol durumu":
- option "Kontrol gerekli" [selected]
- option "Doğrulandı"
- text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
- textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde"
- paragraph: Resmi dokümandan eksik belge gereksinimi ekle
- text: Belge adı
- textbox "Belge adı"
- text: Kaynak doküman ve madde
- textbox "Kaynak doküman ve madde"
- button "Belge gereksinimi ekle" [disabled]
- generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü"
- option "Kontrol gerekli" [selected]
- option "Uygun"
- option "Uygun değil"
- option "Kontrol gerekli" [selected]
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli"
- option "Kontrol gerekli" [selected]
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli"
- option "Kontrol gerekli" [selected]
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli"
- option "Kontrol gerekli" [selected]
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli"
- generic "6 belge/uygunluk kontrolü tamamlanmalı"
- generic: Teklif toplamı (KDV hariç)
- generic: 251,00 TL
- paragraph: "Yazı ile: ikiyüzellibir Türk Lirası"
- heading "Hazırlanacak belgeler" [level=5]
- button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]:
- generic: Teklif Mektubu
- generic: Rakam ve yazı ile bedel
- text: Yazdır / PDF
- button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]:
- generic: Birim Fiyat Teklif Cetveli
- generic: 1 kalem
- text: Yazdır / PDF
- button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]:
- generic: Evrak Kontrol Listesi
- generic: 1 belge
- text: Yazdır / PDF
- paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.
- paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın.
- button
- button "Kullanım kılavuzu (? tuşu)":
- region "Notifications (F8)":
- list
@@ -0,0 +1,72 @@
[level=5]
- paragraph: Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.
- table:
- rowgroup:
- row "Kalem Miktar Birim fiyat Tutar":
- columnheader "Kalem"
- columnheader "Miktar"
- columnheader "Birim fiyat"
- columnheader "Tutar"
- rowgroup:
- row "Sentetik test kalemi 2 adet 125,50 251,00":
- cell "Sentetik test kalemi"
- cell "2 adet"
- cell "125,50":
- textbox "0": 125,50
- cell "251,00"
- region "Belge ve ürün uygunluğu":
- heading "Belge ve ürün uygunluğu" [level=5]
- paragraph: Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.
- group "Sentetik yetki belgesi (zorunlu)":
- generic: Sentetik yetki belgesi (zorunlu)
- paragraph: Yerel QA dokümanı madde 7
- button "Elle eklenen gereksinimi kaldır"
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı" [selected]
- text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
- textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde": QA dosyası v1 — yalnız test, madde 7
- paragraph: Resmi dokümandan eksik belge gereksinimi ekle
- text: Belge adı
- textbox "Belge adı"
- text: Kaynak doküman ve madde
- textbox "Kaynak doküman ve madde"
- button "Belge gereksinimi ekle" [disabled]
- generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü"
- option "Kontrol gerekli"
- option "Uygun" [selected]
- option "Uygun değil"
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- generic: Teklif toplamı (KDV hariç)
- generic: 251,00 TL
- paragraph: "Yazı ile: ikiyüzellibir Türk Lirası"
- heading "Hazırlanacak belgeler" [level=5]
- button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]:
- generic: Teklif Mektubu
- generic: Rakam ve yazı ile bedel
- text: Yazdır / PDF
- button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]:
- generic: Birim Fiyat Teklif Cetveli
- generic: 1 kalem
- text: Yazdır / PDF
- button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]:
- generic: Evrak Kontrol Listesi
- generic: 1 belge
- text: Yazdır / PDF
- paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.
- paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın.
- button
- button "Kullanım kılavuzu (? tuşu)":
- region "Notifications (F8)":
- list
+9 -1
View File
@@ -63,9 +63,17 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- Açık kalan Kanban, sağ panel ve takvim tarih etiketleri 30 saniyede bir güncellenir. Ürün aramasındaki geçmiş tarih filtresi aynı kaynak saatini kullanır; filtre etiketi bilinmeyen saati başvuruya açık garantisi gibi sunmaz. - Açık kalan Kanban, sağ panel ve takvim tarih etiketleri 30 saniyede bir güncellenir. Ürün aramasındaki geçmiş tarih filtresi aynı kaynak saatini kullanır; filtre etiketi bilinmeyen saati başvuruya açık garantisi gibi sunmaz.
- Üç tarih testi gün farkını, Türkiye'de yıl/gece geçişini, tam kapanış anını, eksik saat ve artık günleri doğrular. Google/Outlook gerçek içe aktarımı, zeyilname güncellemesi ve bütün sunucu bildirim zamanlayıcıları henüz bu kapsamda doğrulanmadı. - Üç tarih testi gün farkını, Türkiye'de yıl/gece geçişini, tam kapanış anını, eksik saat ve artık günleri doğrular. Google/Outlook gerçek içe aktarımı, zeyilname güncellemesi ve bütün sunucu bildirim zamanlayıcıları henüz bu kapsamda doğrulanmadı.
## Tarayıcı doğrulaması ve virgüllü fiyat hatası
- Geçici normal hesapla Giresun adlı sentetik kayıtta firma alanları, 120 gün, fiyat ve manuel belge gereksinimi dolduruldu; kaydetme/yenileme sonrası geri geldi. Eksik kontroller varken çıktı düğmeleri kapalı kaldı.
- Ürün modeli, teknik kaynak, belge doğrulaması, sorumlu ve dört sağlık yükümlülüğü kontrolü kaydedilip yenilemeden sonra ekrandan doğrulandı. Resmi kaynak onayı verilmedi; gerçek başvuru veya resmi uygunluk iddiası yok.
- Tarayıcı testi yeni P1 kusuru ortaya çıkardı: `125,50` girdisi virgül silinerek `12550` oluyordu. Girdi artık değiştirilmeden ortak fiyat ayrıştırıcısına gider; nokta/virgül ondalığı desteklenir, geçersiz metin reddedilir. PDF HTML'sinde de aynı ayrıştırıcı kullanılır, birim fiyat hassasiyeti korunur.
- Düzeltme sonrasında tarayıcıda `125,50 × 2 = 251,00 TL` görüldü ve yeniden yüklemede korundu. Bir otomatik test ekran hesabı/PDF HTML tutarlılığı, geçersiz sayı ve aşırı tutarı kapsar.
- Kanıtlar: [taslak yenilemesi](evidence/qa-draft-after-reload.txt), [virgüllü fiyat](evidence/qa-turkish-price-fixed.txt), [kontrol yenilemesi](evidence/qa-checks-after-reload.txt).
## Doğrulama ## Doğrulama
`npm test`: 30/30 geçti (son yeniden çalıştırma: 6 Eylül 2026). `npm test`: 31/31 geçti (son yeniden çalıştırma: 6 Eylül 2026).
`npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor. `npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor.
`node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti. `node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti.
+5 -4
View File
@@ -1,5 +1,5 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { bidRowTotal, validateBidRows, roundMoney } from '../services/bidValidation'; import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource'; import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
import { PreparationChecks } from './PreparationChecks'; import { PreparationChecks } from './PreparationChecks';
import { preparationBlockers, documentKey } from '../services/preparationChecks'; import { preparationBlockers, documentKey } from '../services/preparationChecks';
@@ -91,7 +91,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
}; };
const toplam = teklifTuru === 'goturu' ? (Number(goturuBedel) || 0) : kalemToplami; const toplam = teklifTuru === 'goturu' ? (parseBidPrice(goturuBedel) || 0) : kalemToplami;
const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim()); const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim());
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0; const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
@@ -246,7 +246,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</span> </span>
<input <input
value={goturuBedel} value={goturuBedel}
onChange={(e) => setGoturuBedel(e.target.value.replace(/[^\d.]/g, ''))} onChange={(e) => setGoturuBedel(e.target.value)}
placeholder="0" placeholder="0"
className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300" className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300"
/> />
@@ -254,6 +254,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
) : kalemler.length > 0 ? ( ) : kalemler.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">Kalem fiyatlandırma</h5> <h5 className="font-extrabold text-xs text-slate-900">Kalem fiyatlandırma</h5>
<p className="text-xs text-slate-500">Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.</p>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs min-w-[520px]"> <table className="w-full text-xs min-w-[520px]">
<thead> <thead>
@@ -278,7 +279,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<input <input
value={fiyatlar[r.priceKey] ?? ''} value={fiyatlar[r.priceKey] ?? ''}
onChange={(e) => onChange={(e) =>
setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value.replace(/[^\d.]/g, '') }))} setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value }))}
placeholder={r.estimatedUnitPrice != null ? String(r.estimatedUnitPrice) : '0'} placeholder={r.estimatedUnitPrice != null ? String(r.estimatedUnitPrice) : '0'}
className="w-28 text-xs text-right px-2 py-1 rounded border border-slate-300" className="w-28 text-xs text-right px-2 py-1 rounded border border-slate-300"
/> />
+2 -2
View File
@@ -1,4 +1,4 @@
import { quantityParts, bidRowTotal, validateBidRows } from './bidValidation.js'; import { quantityParts, parseBidPrice, bidRowTotal, validateBidRows } from './bidValidation.js';
/** /**
* İhale dosyası belgeleri — teklif mektubu, birim fiyat teklif cetveli ve * İhale dosyası belgeleri — teklif mektubu, birim fiyat teklif cetveli ve
* evrak kontrol listesi. * evrak kontrol listesi.
@@ -162,7 +162,7 @@ export function teklifCetveliHtml(f, satirlar, toplam) {
if (errors.length) throw new Error(errors.join(" ")); if (errors.length) throw new Error(errors.join(" "));
const govde = satirlar.map((r, i) => { const govde = satirlar.map((r, i) => {
const { miktar, birim } = miktarAyristir(r.quantity); const { miktar, birim } = miktarAyristir(r.quantity);
const bf = Number(r.birimFiyat) || 0; const bf = parseBidPrice(r.birimFiyat);
const tutar = bidRowTotal(r); const tutar = bidRowTotal(r);
return `<tr> return `<tr>
<td class="s">${i + 1}</td> <td class="s">${i + 1}</td>
+14 -2
View File
@@ -1,3 +1,14 @@
// Preserve typed input; never delete a decimal separator and change its value.
export function parseBidPrice(value) {
if (typeof value === 'number') return Number.isFinite(value) && value >= 0 ? value : null;
const text = String(value ?? '').trim();
const plain = /^\d+(?:[.,]\d{1,8})?$/;
const groupedTurkish = /^\d{1,3}(?:\.\d{3})+,\d{1,8}$/;
if (!plain.test(text) && !groupedTurkish.test(text)) return null;
const normalized = text.includes(',') ? text.replace(/\./g,'').replace(',','.') : text;
const result = Number(normalized);
return Number.isFinite(result) ? result : null;
}
export function quantityParts(value) { export function quantityParts(value) {
if (typeof value === 'number') return {miktar:Number.isFinite(value) ? value : null,birim:''}; if (typeof value === 'number') return {miktar:Number.isFinite(value) ? value : null,birim:''};
const match = /^\s*([+-]?[\d]+(?:[.,]\d+)*)\s+(.+?)\s*$/.exec(String(value || '')); const match = /^\s*([+-]?[\d]+(?:[.,]\d+)*)\s+(.+?)\s*$/.exec(String(value || ''));
@@ -11,8 +22,9 @@ export function quantityParts(value) {
export const roundMoney = n => Math.round((n + Number.EPSILON) * 100) / 100; export const roundMoney = n => Math.round((n + Number.EPSILON) * 100) / 100;
export function bidRowTotal(row) { export function bidRowTotal(row) {
const {miktar} = quantityParts(row.quantity); const {miktar} = quantityParts(row.quantity);
const price = Number(row.birimFiyat); const price = parseBidPrice(row.birimFiyat);
return miktar > 0 && Number.isFinite(price) && price > 0 ? roundMoney(miktar * price) : null; const total = miktar * price;
return miktar > 0 && price > 0 && Number.isFinite(total) && total <= Number.MAX_SAFE_INTEGER / 100 ? roundMoney(total) : null;
} }
export function validateBidRows(rows) { export function validateBidRows(rows) {
if (!rows.length) return ['Resmi teklif cetvelindeki kalemler eksik.']; if (!rows.length) return ['Resmi teklif cetvelindeki kalemler eksik.'];
+13 -1
View File
@@ -1,6 +1,6 @@
import {test} from 'node:test'; import {test} from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import {quantityParts,validateBidRows,bidRowTotal} from '../src/services/bidValidation.js'; import {quantityParts,validateBidRows,bidRowTotal,parseBidPrice} from '../src/services/bidValidation.js';
import {yaziylaTL,teklifCetveliHtml} from '../src/services/bidDocs.js'; import {yaziylaTL,teklifCetveliHtml} from '../src/services/bidDocs.js';
test('Turkish quantities, decimal quantities and units are preserved',()=>{ test('Turkish quantities, decimal quantities and units are preserved',()=>{
assert.deepEqual(quantityParts('2.138.330 puan'),{miktar:2138330,birim:'puan'}); assert.deepEqual(quantityParts('2.138.330 puan'),{miktar:2138330,birim:'puan'});
@@ -22,3 +22,15 @@ test('100 rows retain Turkish names and escaped content in printable sheet',()=>
assert.equal((out.match(/Ölçüm &lt;kit&gt;/g)||[]).length,100); assert.equal((out.match(/Ölçüm &lt;kit&gt;/g)||[]).length,100);
assert.ok(out.includes('İğdır'));assert.ok(out.includes('2.000,00')); assert.ok(out.includes('İğdır'));assert.ok(out.includes('2.000,00'));
}); });
test('Turkish decimal price is identical in row total and printable sheet',()=>{
const row={name:'Kit',quantity:'2 adet',birimFiyat:'125,50'};
assert.equal(parseBidPrice(row.birimFiyat),125.5);
assert.equal(parseBidPrice('1.250,50'),1250.5);
assert.equal(bidRowTotal(row),251);
const html=teklifCetveliHtml({},[row],251);
assert.ok(html.includes('125,50'));assert.ok(html.includes('251,00'));
assert.ok(!html.includes('25.100'));
for(const value of ['', '125,,50','1e3','125 TL','1.2.3','-20','125,']) assert.equal(parseBidPrice(value),null);
assert.equal(bidRowTotal({quantity:'1000000000 adet',birimFiyat:'999999999999999'}),null);
});