fix: invalidate pending bid output when preparation changes
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user