diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 12aa532..cbb91bd 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -991,3 +991,9 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Son üye UI üzerinden çıkarılabildi; sayfa yenilenince ekip sayısı sıfır kaldı. Altı kontrol geçti. Kanıt: /Users/hikmettanriverdi/Documents/ChatGPT/ihale/rapor/qa-team-persistence.json. - Üretim kodu değişmedi; önceki 224 test/derleme tekrar edilmedi. Ekip iki sekme yarışı ve yavaş ağdaki bekleme görünümü bu tarayıcı senaryosunda ölçülmedi. Ekip sürüm reddi API testinde doğrulanmış durumda. - Test sekmesi kapatıldı; bu testin QA süreci exit 0 ile sonlandırıldı ve geçici depo cleanup akışı tamamlandı. Dış davet gönderilmedi, gerçek hesap oluşturulmadı. + +### Firma alanlarında ortak biçim kontrolü — 6 Eylül 2026 +- Teklif hazırlığı ve kayıtlı taslak özeti aynı bidFirmIssues fonksiyonunu kullanır. Boş/yalnız boşluk veya nesne/dizi biçimindeki zorunlu değerler tamamlanmış sayılmaz. Dolu fakat biçimi hatalı e-posta da çıktıyı engeller; önceden yalnız boş olup olmadığı denetleniyordu. +- Hata listesi e-posta alanına yönlendirir; alan altında açıklama, aria-invalid ve aria-describedby bağlantısı bulunur. E-posta ve telefon alanları uygun HTML input türlerini kullanır. Geçersiz taslak yine kaydedilebilir; hazır çıktı kapısı açılmaz. +- 226 otomatik test ve derleme geçti. Hatalı adresler, baş/son boşluklar, opsiyonel sicil alanı, zorunlu alanlarda nesne/dizi/boşluk ve hazırlık özetiyle aynı hata hedefi sınandı. +- Bu biçim/tamlık kontrolüdür; vergi kaydı, yetki, telefonun ulaşılabilirliği veya e-posta sahipliği doğrulaması değildir. Gerçek firma uygunluğu ve yeni alan hata görünümünün tarayıcı kabulü açık. Canlıya yayın yapılmadı. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 8dffc5f..fabce03 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -1,4 +1,4 @@ -import { bidFirmFields as ALANLAR } from '../services/bidFirmFields'; +import { bidFirmFields as ALANLAR, bidFirmIssues } from '../services/bidFirmFields'; import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis'; import { preparationDeadlineProblem } from '../services/preparationDeadline'; import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms'; @@ -130,7 +130,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu const toplam = teklifTuru === 'goturu' ? bidAmount(goturuBedel) : kalemToplami; - const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim()); + const eksikler = bidFirmIssues(firma); const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0; const turCeliskisi = !['birim','goturu'].includes(teklifTuru) || kaynakTeklifTuru.status === 'conflict' || (kaynakTeklifTuru.type && teklifTuru !== kaynakTeklifTuru.type); const sureEksik = !Number.isInteger(Number(gecerlilikGun)) || !(Number(gecerlilikGun) > 0) || kaynakGecerlilik.status === 'conflict' || (kaynakGecerlilik.days && Number(gecerlilikGun) < kaynakGecerlilik.days); @@ -146,7 +146,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu ...(kaynakKontrolGerekli ? [{target:'source',text:'Resmi dokümanla kaynak karşılaştırmasını tamamlayın.'}] : []), ...(analizEksik ? [{target:'analysis',text:'Kısmi analizi tam ve doğrulanabilir kaynakla tamamlayın.'}] : []), ...(sourceOptions.needsSelection ? [{target:'itemSource',text:'Resmi cetvele uygun kalem kaynağını seçin.'}] : []), - ...eksikler.map(a=>({target:`firm:${a.k}`,text:`${a.l} bilgisini doldurun.`})), + ...eksikler.map(a=>({target:`firm:${a.k}`,text:a.message})), ...(turCeliskisi ? [{target:'bidType',text:'Teklif türünü kaynak dokümana göre doğrulayın.'}] : []), ...(sureEksik ? [{target:'validity',text:'Teklif geçerlilik süresini doğrulayın.'}] : []), ...lotSelection.errors.map(text=>({target:'lots',text})), @@ -331,7 +331,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
İstekli (firma) bilgileri
{ALANLAR.map((a) => { - const eksik = a.zorunlu && !String(firma[a.k] || '').trim(); + const issue = eksikler.find(field=>field.k===a.k); + const eksik = Boolean(issue); return ( ); })} diff --git a/src/services/bidFirmFields.js b/src/services/bidFirmFields.js index d96c7d8..c48b1dc 100644 --- a/src/services/bidFirmFields.js +++ b/src/services/bidFirmFields.js @@ -9,3 +9,15 @@ export const bidFirmFields = [ { k: 'telefon', l: 'Telefon', zorunlu: true }, { k: 'eposta', l: 'E-posta', zorunlu: true }, ]; + +// Form-level completeness and syntax only; no registry or legal eligibility check. +export function bidFirmIssues(firma={}) { + return bidFirmFields.flatMap(field=>{ + const raw=firma?.[field.k]; + const text=typeof raw==='string' || typeof raw==='number' ? String(raw).trim() : ''; + if(field.zorunlu && !text)return [{...field,message:`${field.l} bilgisini doldurun.`}]; + if(field.k==='eposta' && text && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)) + return [{...field,message:'E-posta adresini ad@firma.com biçiminde girin.'}]; + return []; + }); +} diff --git a/src/services/preparationSummary.js b/src/services/preparationSummary.js index b2be417..93ac432 100644 --- a/src/services/preparationSummary.js +++ b/src/services/preparationSummary.js @@ -1,4 +1,4 @@ -import {bidFirmFields} from './bidFirmFields.js'; +import {bidFirmIssues} from './bidFirmFields.js'; import {sourceRows,bidSourceOptions,bidSourceSnapshot,restoreBidPrices} from './bidSource.js'; import {selectBidLots} from './bidLots.js'; import {bidTotal,bidAmount,validateBidRows} from './bidValidation.js'; @@ -21,7 +21,7 @@ export function preparationSummary(tender,vault,now=new Date()) { if(draft.reviewedSource!==snapshot)add('source','Resmi kaynak karşılaştırması gerekli.'); if(analysis.sourceReview?.status==='partial')add('analysis','Analiz kısmi; kaynak kontrolünü tamamlayın.'); if(options.needsSelection)add('itemSource','Resmi cetvele uygun kalem listesini seçin.'); - for(const field of bidFirmFields)if(field.zorunlu&&!String(draft.firma?.[field.k] || '').trim())add(`firm:${field.k}`,`${field.l} kayıtlı taslakta eksik.`); + for(const issue of bidFirmIssues(draft.firma))add(`firm:${issue.k}`,issue.message); const type=bidTypeForForm(tender.aciklama,analysis.procurementTerms?.bidType),bidType=draft.teklifTuru || type.type || ''; if(!['birim','goturu'].includes(bidType)||type.status==='conflict'||(type.type&&type.type!==bidType))add('bidType','Teklif türünü doğrulayın.'); const validity=bidValidityForForm(tender.aciklama,analysis.procurementTerms?.bidValidity),days=Number(draft.gecerlilikGun || validity.days || ''); diff --git a/tests/bid-firm.test.mjs b/tests/bid-firm.test.mjs new file mode 100644 index 0000000..2fb9ec7 --- /dev/null +++ b/tests/bid-firm.test.mjs @@ -0,0 +1,21 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {bidFirmFields,bidFirmIssues} from '../src/services/bidFirmFields.js'; +import {preparationSummary} from '../src/services/preparationSummary.js'; +const firm=Object.fromEntries(bidFirmFields.map(f=>[f.k,f.k==='eposta'?'teklif+lab@example.test':'QA metin'])); +test('filled but malformed email blocks both firm form and saved preparation summary',()=>{ + assert.deepEqual(bidFirmIssues(firm),[]); + for(const eposta of ['yanlış','firma@','@firma.com','a b@firma.com','a@@firma.com']){ + const firma={...firm,eposta};const issues=bidFirmIssues(firma); + assert.equal(issues.length,1);assert.equal(issues[0].k,'eposta'); + const summary=preparationSummary({bidDraft:{firma}},{documents:[]}); + assert.ok(summary.issues.some(x=>x.target==='firm:eposta'&&x.text===issues[0].message)); + } + assert.deepEqual(bidFirmIssues({...firm,eposta:' teklif@example.test '}),[]); +}); +test('whitespace and non-scalar required values are incomplete; optional registry field stays optional',()=>{ + for(const field of bidFirmFields.filter(f=>f.zorunlu)){ + for(const bad of [' ',null,{},[]])assert.ok(bidFirmIssues({...firm,[field.k]:bad}).some(x=>x.k===field.k)); + } + assert.deepEqual(bidFirmIssues({...firm,sicilNo:''}),[]); +});