fix: enforce source-backed offer type in bid preparation

This commit is contained in:
2026-09-06 09:25:51 +03:00
parent a751147d45
commit bc71984847
5 changed files with 51 additions and 7 deletions
+11 -4
View File
@@ -1,4 +1,4 @@
import { bidValidityForForm } from '../services/bidTerms';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
import { getToken } from '../services/auth';
import { listDocuments, downloadDocument } from '../services/account';
@@ -71,8 +71,9 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
};
});
const [teklifTuru, setTeklifTuru] = useState(storedDraft.teklifTuru || 'birim');
const effectiveResults = results || storedDraft.analysisSource || {};
const kaynakTeklifTuru = bidTypeForForm(tender?.aciklama,effectiveResults.procurementTerms?.bidType);
const [teklifTuru, setTeklifTuru] = useState(storedDraft.teklifTuru || kaynakTeklifTuru.type || '');
const kaynakGecerlilik = bidValidityForForm(tender?.aciklama, effectiveResults.procurementTerms?.bidValidity);
const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik.days || '');
@@ -124,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 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 kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)});
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
@@ -234,6 +235,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>}
{kaynakTeklifTuru.evidence.length > 0 && <div className="text-xs border rounded-lg p-3 space-y-1">
<p className="font-bold">Teklif türünün kaynağı</p>
{kaynakTeklifTuru.status === 'conflict' && <p role="alert" className="text-red-700">Kaynaklarda farklı teklif türleri var. Güncel resmi dokümanı ve karma kapsamı kontrol edin.</p>}
{[...new Set(kaynakTeklifTuru.evidence.map(e=>e.quote))].map(quote=><blockquote key={quote}>{quote}</blockquote>)}
</div>}
{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>}
@@ -289,6 +295,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
onChange={(e) => setTeklifTuru(e.target.value)}
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300 bg-white"
>
<option value="">Resmi dokümana göre seçin</option>
<option value="birim">Birim fiyat</option>
<option value="goturu">Anahtar teslim götürü bedel</option>
</select>
@@ -428,7 +435,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
{(kalemEksik || turCeliskisi || sureEksik) && <p role="alert" className="text-xs bg-amber-50 p-3 text-amber-900">
{kalemEksik && 'Seçilen her kalemde ad, miktar, birim ve fiyat gerekli. '}
{turCeliskisi && 'İlan birim fiyat teklif belirtiyor; götürü bedel seçilemez. '}
{turCeliskisi && 'Teklif türünü resmi dokümana göre seçin; kaynakla farklılık varsa giderin. '}
{sureEksik && 'Teklif geçerlilik süresini kaynak dokümana göre doğrulayın. '}
</p>}
{/* Belgeler */}
+16
View File
@@ -17,3 +17,19 @@ export function bidValidityForForm(announcement, analyzed) {
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};
}
export function extractBidType(text) {
const source=String(text || ''),lower=source.toLocaleLowerCase('tr-TR'),evidence=[];
for(const [type,label] of [['birim','birim\\s+fiyat'],['goturu','(?:anahtar\\s+teslimi?\\s+)?götürü\\s+bedel']]) {
const pattern=new RegExp(`(?:teklif\\s+türü\\s*[::-]\\s*${label}|${label}\\s+(?:üzerinden\\s+|şeklinde\\s+)?teklif\\s+(?:verilecektir|vereceklerdir|sunulacaktır|alınacaktır))`,'gu');
for(const match of lower.matchAll(pattern)) evidence.push({type,quote:source.slice(match.index,match.index+match[0].length),start:match.index,end:match.index+match[0].length});
}
return summarizeBidType(evidence);
}
function summarizeBidType(evidence) {
const types=[...new Set(evidence.map(e=>e.type).filter(t=>['birim','goturu'].includes(t)))];
return {status:types.length>1?'conflict':types.length?'found':'not_found',type:types.length===1?types[0]:null,evidence};
}
export function bidTypeForForm(announcement,analyzed) {
return summarizeBidType([...extractBidType(announcement).evidence,...(Array.isArray(analyzed?.evidence)?analyzed.evidence:[])]);
}