fix: invalidate pending bid output when preparation changes

This commit is contained in:
2026-09-06 08:54:22 +03:00
parent 953d3b5c1e
commit 2dfd32aa9c
3 changed files with 35 additions and 10 deletions
+4
View File
@@ -108,3 +108,7 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- D11: küçük sürümlerle yayın, yedek/geri dönüş ve canlı ölçümler. - D11: küçük sürümlerle yayın, yedek/geri dönüş ve canlı ölçümler.
Kod testleri bütün planın tamamlandığını kanıtlamaz. Tarayıcı kanıtı şu an yalnız sentetik görev kalıcılığı senaryosunu kapsar; kapsamlı uçtan uca ve canlı kabul henüz yoktur. Kod testleri bütün planın tamamlandığını kanıtlamaz. Tarayıcı kanıtı şu an yalnız sentetik görev kalıcılığı senaryosunu kapsar; kapsamlı uçtan uca ve canlı kabul henüz yoktur.
## Çıktı doğrulaması sırasındaki değişiklikler
Dosya kontrolü sürerken form/kaynak veya oturum değişirse çıktı iptal edilir. Kontrol penceresi tıklama sırasında açılır; doğrulama hatasında kapatılır. Aynı anda ikinci çıktı başlatılmaz. Taslak kaydı sürerken yapılan yeni değişiklikler, önceki kaydın yanıtı geldiğinde kaydedilmiş sayılmaz. Mevcut 35 test ve derleme geçti; bu yarış senaryolarının gecikmeli tarayıcı kabulü ayrıca bekliyor.
+27 -8
View File
@@ -1,6 +1,7 @@
import { getToken } from '../services/auth';
import { listDocuments, downloadDocument } from '../services/account'; import { listDocuments, downloadDocument } from '../services/account';
import { tenderDateKey } from '../services/tenderCalendar'; import { tenderDateKey } from '../services/tenderCalendar';
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect, useRef } from 'react';
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } 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 { rowLot, selectBidLots } from '../services/bidLots'; import { rowLot, selectBidLots } from '../services/bidLots';
@@ -100,9 +101,16 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
); );
const kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]); const kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]);
const latestDraft = useRef('');
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid]);
const mounted = useRef(true);
const printWindow = useRef(null);
const [printing,setPrinting] = useState(false);
useEffect(()=>{mounted.current=true;return ()=>{mounted.current=false;printWindow.current?.close();};},[]);
const saveDraft = async () => { const saveDraft = async () => {
const submitted = latestDraft.current;
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}}); const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}});
if (saved) { setDirty(false); setHata(null); } if (saved && mounted.current) { setDirty(latestDraft.current !== submitted); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
}; };
@@ -127,21 +135,32 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
}); });
const uret = async (tip) => { const uret = async (tip) => {
if (printWindow.current) return;
const submitted = latestDraft.current;
const session = getToken();
setHata(null); setHata(null);
try { try {
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.
printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>');
setPrinting(true);
const currentFiles = await listDocuments(); const currentFiles = await listDocuments();
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form, kaynak veya oturum değişti. Güncel bilgilerle yeniden çıktı oluşturun.');
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender)}; const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender)};
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 f = formVerisi(); const f = formVerisi();
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam)); if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam), printWindow.current);
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam)); if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam), printWindow.current);
if (tip === 'liste') belgeYazdir('Evrak Kontrol Listesi', evrakListesiHtml(f, documents)); if (tip === 'liste') belgeYazdir('Evrak Kontrol Listesi', evrakListesiHtml(f, documents), printWindow.current);
printWindow.current = null;
} catch (e) { } catch (e) {
setHata(e.message); printWindow.current?.close();
} printWindow.current = null;
if(mounted.current) setHata(e.message);
} finally { if(mounted.current) setPrinting(false); }
}; };
return ( return (
@@ -389,7 +408,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<button <button
key={b.t} key={b.t}
type="button" type="button"
disabled={!hazir} disabled={!hazir || printing}
onClick={() => uret(b.t)} onClick={() => uret(b.t)}
className="text-left bg-white border border-slate-300 hover:border-emerald-500 hover:bg-emerald-50/50 disabled:opacity-40 disabled:hover:border-slate-300 disabled:hover:bg-white rounded-xl p-3 transition-colors" className="text-left bg-white border border-slate-300 hover:border-emerald-500 hover:bg-emerald-50/50 disabled:opacity-40 disabled:hover:border-slate-300 disabled:hover:bg-white rounded-xl p-3 transition-colors"
> >
+4 -2
View File
@@ -84,9 +84,11 @@ const STIL = `
`; `;
/** Belgeyi kendi penceresinde açıp yazdırma diyaloğunu tetikler. */ /** Belgeyi kendi penceresinde açıp yazdırma diyaloğunu tetikler. */
export function belgeYazdir(baslik, govde) { export function belgeYazdir(baslik, govde, existingWindow) {
const w = window.open('', '_blank', 'width=900,height=1000'); const w = existingWindow || window.open('', '_blank', 'width=900,height=1000');
if (!w) throw new Error('Tarayıcı yeni pencereyi engelledi. Açılır pencere iznini verip tekrar deneyin.'); if (!w) throw new Error('Tarayıcı yeni pencereyi engelledi. Açılır pencere iznini verip tekrar deneyin.');
if(w.closed) throw new Error('Çıktı penceresi kapatıldı. Yeniden deneyin.');
w.document.open();
w.document.write(`<!doctype html><html lang="tr"><head><meta charset="utf-8"> w.document.write(`<!doctype html><html lang="tr"><head><meta charset="utf-8">
<title>${kacir(baslik)}</title><style>${STIL}</style></head><body>${govde} <title>${kacir(baslik)}</title><style>${STIL}</style></head><body>${govde}
<div class="yazdirma" style="text-align:center;margin-top:10mm"> <div class="yazdirma" style="text-align:center;margin-top:10mm">