fix: validate firm email consistently before bid output
This commit is contained in:
@@ -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.
|
- 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.
|
- Ü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ı.
|
- 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ı.
|
||||||
|
|||||||
@@ -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 { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
|
||||||
import { preparationDeadlineProblem } from '../services/preparationDeadline';
|
import { preparationDeadlineProblem } from '../services/preparationDeadline';
|
||||||
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
|
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 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 kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
||||||
const turCeliskisi = !['birim','goturu'].includes(teklifTuru) || kaynakTeklifTuru.status === 'conflict' || (kaynakTeklifTuru.type && teklifTuru !== kaynakTeklifTuru.type);
|
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);
|
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.'}] : []),
|
...(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.'}] : []),
|
...(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.'}] : []),
|
...(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.'}] : []),
|
...(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.'}] : []),
|
...(sureEksik ? [{target:'validity',text:'Teklif geçerlilik süresini doğrulayın.'}] : []),
|
||||||
...lotSelection.errors.map(text=>({target:'lots',text})),
|
...lotSelection.errors.map(text=>({target:'lots',text})),
|
||||||
@@ -331,7 +331,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
<h5 className="font-extrabold text-xs text-slate-900">İstekli (firma) bilgileri</h5>
|
<h5 className="font-extrabold text-xs text-slate-900">İstekli (firma) bilgileri</h5>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||||
{ALANLAR.map((a) => {
|
{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 (
|
return (
|
||||||
<label key={a.k} className={a.genis ? 'sm:col-span-2' : ''}>
|
<label key={a.k} className={a.genis ? 'sm:col-span-2' : ''}>
|
||||||
<span className="text-[11px] font-semibold text-slate-600 block mb-1">
|
<span className="text-[11px] font-semibold text-slate-600 block mb-1">
|
||||||
@@ -340,6 +341,9 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
<input
|
<input
|
||||||
ref={targetRef(`firm:${a.k}`)}
|
ref={targetRef(`firm:${a.k}`)}
|
||||||
aria-invalid={eksik || undefined}
|
aria-invalid={eksik || undefined}
|
||||||
|
type={a.k==='eposta' ? 'email' : a.k==='telefon' ? 'tel' : 'text'}
|
||||||
|
aria-invalid={eksik}
|
||||||
|
aria-describedby={eksik ? `${fieldPrefix}-firm-error-${a.k}` : undefined}
|
||||||
value={firma[a.k] || ''}
|
value={firma[a.k] || ''}
|
||||||
onChange={(e) => alanYaz(a.k, e.target.value)}
|
onChange={(e) => alanYaz(a.k, e.target.value)}
|
||||||
placeholder={a.ph || ''}
|
placeholder={a.ph || ''}
|
||||||
@@ -347,6 +351,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocu
|
|||||||
eksik ? 'border-amber-400 bg-amber-50/40' : 'border-slate-300'
|
eksik ? 'border-amber-400 bg-amber-50/40' : 'border-slate-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
{issue&&<p id={`${fieldPrefix}-firm-error-${a.k}`} className="text-xs text-amber-800 mt-1">{issue.message}</p>}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -9,3 +9,15 @@ export const bidFirmFields = [
|
|||||||
{ k: 'telefon', l: 'Telefon', zorunlu: true },
|
{ k: 'telefon', l: 'Telefon', zorunlu: true },
|
||||||
{ k: 'eposta', l: 'E-posta', 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 [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {bidFirmFields} from './bidFirmFields.js';
|
import {bidFirmIssues} from './bidFirmFields.js';
|
||||||
import {sourceRows,bidSourceOptions,bidSourceSnapshot,restoreBidPrices} from './bidSource.js';
|
import {sourceRows,bidSourceOptions,bidSourceSnapshot,restoreBidPrices} from './bidSource.js';
|
||||||
import {selectBidLots} from './bidLots.js';
|
import {selectBidLots} from './bidLots.js';
|
||||||
import {bidTotal,bidAmount,validateBidRows} from './bidValidation.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(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(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.');
|
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 || '';
|
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.');
|
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 || '');
|
const validity=bidValidityForForm(tender.aciklama,analysis.procurementTerms?.bidValidity),days=Number(draft.gecerlilikGun || validity.days || '');
|
||||||
|
|||||||
@@ -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:''}),[]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user