fix: carry source-backed bid validity into preparation drafts

This commit is contained in:
2026-09-06 09:24:38 +03:00
parent e07be66e7f
commit a751147d45
6 changed files with 66 additions and 6 deletions
+12 -5
View File
@@ -1,3 +1,4 @@
import { bidValidityForForm } from '../services/bidTerms';
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
import { getToken } from '../services/auth';
import { listDocuments, downloadDocument } from '../services/account';
@@ -71,12 +72,13 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
});
const [teklifTuru, setTeklifTuru] = useState(storedDraft.teklifTuru || 'birim');
const kaynakGecerlilik = /(?:geçerlilik[^.]{0,100}?|ihale tarihinden itibaren\s*)(\d+)\s*(?:takvim\s*)?gün/i.exec(tender?.aciklama || '');
const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik?.[1] || '');
const effectiveResults = results || storedDraft.analysisSource || {};
const kaynakGecerlilik = bidValidityForForm(tender?.aciklama, effectiveResults.procurementTerms?.bidValidity);
const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik.days || '');
const [kisim, setKisim] = useState(storedDraft.kisim || '');
const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || '');
const effectiveResults = results || storedDraft.analysisSource || {};
const [itemSource, setItemSource] = useState(storedDraft.itemSource || {});
const sourceOptions = bidSourceOptions(tender, effectiveResults, itemSource);
const [manualDocuments, setManualDocuments] = useState(storedDraft.manualDocuments || []);
@@ -112,7 +114,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
useEffect(()=>{mounted.current=true;return ()=>{mounted.current=false;printWindow.current?.close();};},[]);
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,itemSource,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null,sourceReview:effectiveResults.sourceReview || null}}});
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null,procurementTerms:effectiveResults.procurementTerms || null,sourceReview:effectiveResults.sourceReview || null}}});
if (!mounted.current) return;
if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
@@ -123,7 +125,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim());
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
const sureEksik = !Number.isInteger(Number(gecerlilikGun)) || !(Number(gecerlilikGun) > 0) || kaynakGecerlilik.status === 'conflict' || (kaynakGecerlilik.days && Number(gecerlilikGun) < kaynakGecerlilik.days);
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)});
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;
@@ -232,6 +234,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</label>
{sourceOptions.needsSelection && <p role="alert" className="text-xs text-red-700">Kaynak seçilmeden çıktı hazırlanamaz. Kaynak listeleri değiştiğinde seçim yeniden yapılmalıdır.</p>}
</fieldset>}
{kaynakGecerlilik.evidence.length > 0 && <div className="text-xs border rounded-lg p-3 space-y-1">
<p className="font-bold">Teklif geçerlilik süresinin kaynağı</p>
{kaynakGecerlilik.status === 'conflict' && <p role="alert" className="text-red-700">Kaynaklarda farklı süreler var. Güncel resmi dokümanı belirleyip analizi yenileyin; çelişki çözülmeden çıktı alınamaz.</p>}
{[...new Set(kaynakGecerlilik.evidence.map(e=>e.quote))].map(quote=><blockquote key={quote}>{quote}</blockquote>)}
</div>}
{analizEksik && <p role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak veya kalem listesi eksik. Tam dokümanı ve resmi cetveli kontrol ederek analizi tamamlayın.</p>}
{/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && (
+1
View File
@@ -32,6 +32,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res
announcement: tender?.aciklama || '',
deadline: [tender?.tarih || '', tender?.saat || ''],
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
procurementTerms: results?.procurementTerms || null,
analyzedItems: results?.itemsAndSpecs || [],
rows: rows.map(r => r.priceKey).sort(),
documents: results?.documentChecklist || [],
+19
View File
@@ -0,0 +1,19 @@
// Extract only explicitly labelled bid validity clauses; delivery/warranty days are unrelated.
export function extractBidValidity(text) {
const source=String(text || '');
const pattern=/teklif(?:lerin|in)?\s+geçerlilik\s+süresi[^\n.\d]{0,100}(\d{1,4})(?:\s*\([^)]{0,80}\))?\s*(?:takvim\s*)?gün/giu;
const evidence=[];
for(const match of source.toLocaleLowerCase('tr-TR').matchAll(pattern)) {
const days=Number(match[1]);
if(days>0) evidence.push({days,quote:source.slice(match.index,match.index+match[0].length),start:match.index,end:match.index+match[0].length});
}
const values=[...new Set(evidence.map(e=>e.days))];
return {status:values.length>1?'conflict':values.length?'found':'not_found',days:values.length===1?values[0]:null,evidence};
}
export function bidValidityForForm(announcement, analyzed) {
const local=extractBidValidity(announcement);
const evidence=[...local.evidence,...(Array.isArray(analyzed?.evidence)?analyzed.evidence:[])];
const values=[...new Set(evidence.map(e=>e.days).filter(d=>Number.isInteger(d)&&d>0))];
return {status:values.length>1?'conflict':values.length?'found':'not_found',days:values.length===1?values[0]:null,evidence};
}