diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index d77fda9..ea27725 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -571,3 +571,11 @@ Doğrulama: mevcut 124 test ve üretim derlemesi başarılı. Bu tur yeni birim Listeye dönüp QA Kayseri açıldığında önceki dosya adı, PDF metni ve inceleme paneli görünmedi; yeni ihale “Henüz belge yüklemediniz” durumunu gösterdi. QA Giresun tekrar açıldığında kayıtlı PDF yerindeydi, önizleme kendiliğinden açık kalmadı. Böylece belgeyi silmeden geçici panel durumunun ihale bazında temizlendiği doğrulandı. Kanıt: `qa-tender-document-switch.json` (önizleme, diğer ihale ve ilk ihaleye dönüş DOM metinleri; yedi kabul kontrolü doğru). Bu test tamamlanmış düz metin PDF okumasından sonraki geçiştir; hâlen süren OCR iptalini veya hesap değişimini doğrulamaz. Gerçek firma belgesi/başvuru kullanılmadı. Bu tur kod değişikliği olmadığı için önceki 124 test ve build yeniden çalıştırılmadı. Planın kalan kabul işleri ve canlı yayın açık. + +### D02 / D03 — Son teklif anında hazırlık ve çıktı kontrolü + +Hazırlık formunun hazır hesabına doğrulanmış gelecek tarih/saat kontrolü eklendi. Saati/tarihi bilinmeyen, süresi geçen, iptal/sonuçlanmış ihale için hazır teklif çıktısı kapalıdır; kullanıcı taslak kaydetmeye devam edebilir. Ekran durumu otuz saniyede yenilenir; çıktı tıklamasında, güncel belge kontrolünden sonra ve Excel dosyası hazır olduğunda gerçek saat yeniden kontrol edilir. Böylece dosya üretilirken son teklif anı geçerse eski hazır durumuyla indirmeye devam edilmez. + +Tarih/saat ayrıştırması hatalı bitişik ekleri (ör. `10:0099`) geçerli saat kabul etmez. Mevcut Türkiye saat dilimi ve kaynak tarih doğrulaması kullanılır. + +Testler: son teklif anından bir milisaniye önce/sonra; bilinmeyen/geçersiz tarih ve saat; gelecekte tarih taşısa da iptal/sonuçlanmış kayıt. Tüm 127 test ve üretim derlemesi geçti. Gerçek tarayıcıda zamanı geçen uzun çıktı kabulü henüz yapılmadı. Büyük paket uyarıları sürüyor. Planın diğer işleri ve canlı yayın açık. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 26ce889..1fc0b50 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -1,3 +1,4 @@ +import { preparationDeadlineProblem } from '../services/preparationDeadline'; import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms'; import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook'; import { getToken } from '../services/auth'; @@ -127,9 +128,12 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { 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); + const [deadlineNow,setDeadlineNow] = useState(() => new Date()); + useEffect(() => {const timer=setInterval(()=>setDeadlineNow(new Date()),30000);return ()=>clearInterval(timer);},[]); + const deadlineProblem = preparationDeadlineProblem(tender,deadlineNow); const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn}); const analizEksik = effectiveResults.sourceReview?.status === 'partial'; - const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection; + const hazir = !deadlineProblem && eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection; const formVerisi = () => ({ ...firma, @@ -148,6 +152,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { const session = getToken(); setHata(null); try { + const initialDeadlineProblem=preparationDeadlineProblem(tender); + if(initialDeadlineProblem)throw new Error(initialDeadlineProblem); if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.'); // Open during the click; async validation must not lose popup activation. printWindow.current = belgeYazdir('Dosya kontrolü', '

Belgeler kontrol ediliyor…

'); @@ -161,12 +167,16 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { setVault(currentVault); const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault); if (fileErrors.length) throw new Error(fileErrors.join(' ')); + const currentDeadlineProblem=preparationDeadlineProblem(tender); + if(currentDeadlineProblem)throw new Error(currentDeadlineProblem); const f = formVerisi(); if (tip === 'excel') { const buffer = await createBidWorkbook(f,satirlar); if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn); if (!mounted.current || session !== getToken() || submitted !== latestDraft.current) throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.'); + const finalDeadlineProblem=preparationDeadlineProblem(tender); + if(finalDeadlineProblem)throw new Error(finalDeadlineProblem); downloadBidWorkbook(buffer,tender.ikn); printWindow.current?.close(); } @@ -251,6 +261,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { {effectiveResults.sourceReview?.humanReview &&

Metin incelemesi: sürüm {effectiveResults.sourceReview.humanReview.revision} · {effectiveResults.sourceReview.humanReview.reviewedBy} · {effectiveResults.sourceReview.humanReview.reviewedAt}. Kullanıcı karşılaştırmasıdır; resmi onay değildir.

} + {deadlineProblem &&

{deadlineProblem}

} {analizEksik &&

Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.

} {/* Sizden beklenen bilgiler */} {eksikler.length > 0 && ( diff --git a/src/services/preparationDeadline.js b/src/services/preparationDeadline.js new file mode 100644 index 0000000..86faf19 --- /dev/null +++ b/src/services/preparationDeadline.js @@ -0,0 +1,12 @@ +import {deadlineInfo} from './tenderCalendar.js'; + +/** Preparation may be saved, but a current bid needs a verified future deadline. */ +export function preparationDeadlineProblem(tender, now = new Date()) { + const status=String(tender?.durum || '').toLocaleLowerCase('tr-TR'); + if (/iptal|sonuçlandı|sözleşme imzalandı/.test(status)) + return 'İhale iptal edilmiş veya sonuçlanmış. Hazır teklif çıktısı oluşturulamaz; taslağınızı kaydedebilirsiniz.'; + const info=deadlineInfo(tender,now); + if (!info.deadline) return 'Son teklif tarihi veya saati doğrulanamadı. Kaynak ilanı kontrol edin; taslağınızı kaydedebilirsiniz.'; + if (info.expired) return 'Son teklif zamanı geçti. Hazır teklif çıktısı oluşturulamaz; taslağınızı kaydedebilirsiniz.'; + return null; +} diff --git a/src/services/tenderCalendar.js b/src/services/tenderCalendar.js index a8848f4..4de0a85 100644 --- a/src/services/tenderCalendar.js +++ b/src/services/tenderCalendar.js @@ -2,10 +2,10 @@ // Never manufacture a deadline when the source omitted its time. export function tenderDeadline(t) { const source = String(t.aciklama || ''); - const date = /^(\d{2})\.(\d{2})\.(\d{4})/.exec(String(t.tarih || '')); + const date = /^(\d{2})\.(\d{2})\.(\d{4})(?:$|\s)/.exec(String(t.tarih || '')); if (!date) throw new Error('İhale tarihi doğrulanamadı; takvime eklenemedi.'); const [,day,month,year] = date; - const time = /^(\d{2}):(\d{2})/.exec(String(t.saat || '').trim()) + const time = /^(\d{2}):(\d{2})(?:$|\s)/.exec(String(t.saat || '').trim()) || new RegExp(`${day}\\.${month}\\.${year}\\s*[-–]?\\s*(\\d{2}):(\\d{2})`).exec(source); if (!time) throw new Error('İhale saati kaynakta bulunamadı; önce tarih ve saati doğrulayın.'); const hour = Number(time[1]), minute = Number(time[2]); diff --git a/tests/preparation-deadline.test.mjs b/tests/preparation-deadline.test.mjs new file mode 100644 index 0000000..d7ea65e --- /dev/null +++ b/tests/preparation-deadline.test.mjs @@ -0,0 +1,16 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {preparationDeadlineProblem} from '../src/services/preparationDeadline.js'; +const tender={tarih:'10.09.2026',saat:'10:00',durum:'Açık'}; +test('readiness closes at the exact Istanbul deadline, including during output generation',()=>{ + assert.equal(preparationDeadlineProblem(tender,new Date('2026-09-10T06:59:59.999Z')),null); + assert.match(preparationDeadlineProblem(tender,new Date('2026-09-10T07:00:00Z')),/zamanı geçti/); +}); +test('unknown or malformed deadline cannot qualify as a ready bid',()=>{ + for(const value of [{tarih:'31.02.2026',saat:'10:00'},{tarih:'10.09.2026'},{tarih:'10.09.2026abc',saat:'10:00'},{tarih:'10.09.2026',saat:'10:0099'}]) + assert.match(preparationDeadlineProblem(value,new Date('2026-09-01')),/doğrulanamadı/); +}); +test('cancelled and completed tenders do not regain readiness merely through a future date',()=>{ + for(const durum of ['İptal Edildi','Sonuçlandı','Sözleşme imzalandı']) + assert.match(preparationDeadlineProblem({...tender,durum},new Date('2026-09-01')),/iptal edilmiş veya sonuçlanmış/); +});