Recheck tender deadlines before preparation output delivery

This commit is contained in:
2026-09-06 10:29:17 +03:00
parent cb231426e8
commit dbe43c241d
5 changed files with 50 additions and 3 deletions
+8
View File
@@ -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ı. 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. 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.
+12 -1
View File
@@ -1,3 +1,4 @@
import { preparationDeadlineProblem } from '../services/preparationDeadline';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms'; import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook'; import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
import { getToken } from '../services/auth'; import { getToken } from '../services/auth';
@@ -127,9 +128,12 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
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);
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 kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
const analizEksik = effectiveResults.sourceReview?.status === 'partial'; 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 = () => ({ const formVerisi = () => ({
...firma, ...firma,
@@ -148,6 +152,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const session = getToken(); const session = getToken();
setHata(null); setHata(null);
try { 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.'); 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. // Open during the click; async validation must not lose popup activation.
printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>'); printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>');
@@ -161,12 +167,16 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
setVault(currentVault); setVault(currentVault);
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault); const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
if (fileErrors.length) throw new Error(fileErrors.join(' ')); if (fileErrors.length) throw new Error(fileErrors.join(' '));
const currentDeadlineProblem=preparationDeadlineProblem(tender);
if(currentDeadlineProblem)throw new Error(currentDeadlineProblem);
const f = formVerisi(); const f = formVerisi();
if (tip === 'excel') { if (tip === 'excel') {
const buffer = await createBidWorkbook(f,satirlar); const buffer = await createBidWorkbook(f,satirlar);
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn); if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current) if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.'); 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); downloadBidWorkbook(buffer,tender.ikn);
printWindow.current?.close(); printWindow.current?.close();
} }
@@ -251,6 +261,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
{effectiveResults.sourceReview?.humanReview && <p className="text-xs text-slate-600"> {effectiveResults.sourceReview?.humanReview && <p className="text-xs text-slate-600">
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. 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.
</p>} </p>}
{deadlineProblem && <p role="alert" className="text-xs text-red-700">{deadlineProblem}</p>}
{analizEksik && <p role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.</p>} {analizEksik && <p role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.</p>}
{/* Sizden beklenen bilgiler */} {/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && ( {eksikler.length > 0 && (
+12
View File
@@ -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;
}
+2 -2
View File
@@ -2,10 +2,10 @@
// Never manufacture a deadline when the source omitted its time. // Never manufacture a deadline when the source omitted its time.
export function tenderDeadline(t) { export function tenderDeadline(t) {
const source = String(t.aciklama || ''); 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.'); if (!date) throw new Error('İhale tarihi doğrulanamadı; takvime eklenemedi.');
const [,day,month,year] = date; 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); || 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.'); 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]); const hour = Number(time[1]), minute = Number(time[2]);
+16
View File
@@ -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ış/);
});