fix: require explicit choice for conflicting bid item sources
This commit is contained in:
@@ -4,7 +4,7 @@ import { listDocuments, downloadDocument } from '../services/account';
|
||||
import { tenderDateKey } from '../services/tenderCalendar';
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
|
||||
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
|
||||
import { sourceRows, bidSourceSnapshot, restoreBidPrices, bidSourceOptions } from '../services/bidSource';
|
||||
import { rowLot, selectBidLots } from '../services/bidLots';
|
||||
import { PreparationChecks } from './PreparationChecks';
|
||||
import { preparationBlockers, documentKey } from '../services/preparationChecks';
|
||||
@@ -77,14 +77,16 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
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 || []);
|
||||
const [checks, setChecks] = useState(storedDraft.checks || {});
|
||||
const documents = [...new Map([...(effectiveResults.documentChecklist || []), ...manualDocuments].map(d => [documentKey(d),d])).values()];
|
||||
const [partialBid,setPartialBid] = useState(storedDraft.partialBid || {enabled:false,source:'',selected:[],assignments:{}});
|
||||
const kalemler = sourceRows(tender, effectiveResults);
|
||||
const kalemler = sourceRows(tender, effectiveResults, sourceOptions.selected || 'tender');
|
||||
const lotSelection = selectBidLots(kalemler,partialBid);
|
||||
const allLots = [...new Set(kalemler.map(r=>rowLot(r,partialBid.assignments)).filter(Boolean))];
|
||||
const sourceSnapshot = JSON.stringify([bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler), partialBid]);
|
||||
const sourceSnapshot = JSON.stringify([bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler), partialBid, itemSource, sourceOptions.signature]);
|
||||
const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null);
|
||||
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
|
||||
const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot;
|
||||
@@ -103,15 +105,16 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
|
||||
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]);
|
||||
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource]);
|
||||
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 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,sourceReview:effectiveResults.sourceReview || null}}});
|
||||
if (saved && mounted.current) { setDirty(latestDraft.current !== submitted); setHata(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,sourceReview:effectiveResults.sourceReview || null}}});
|
||||
if (!mounted.current) return;
|
||||
if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); }
|
||||
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
|
||||
};
|
||||
|
||||
@@ -123,7 +126,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
|
||||
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;
|
||||
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
|
||||
|
||||
const formVerisi = () => ({
|
||||
...firma,
|
||||
@@ -214,6 +217,21 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{sourceOptions.conflict && <fieldset className="rounded-xl border border-amber-400 bg-amber-50 p-3 space-y-2">
|
||||
<legend className="text-sm font-bold">Kalem kaynakları farklı</legend>
|
||||
<p className="text-xs">İhale havuzundaki liste ile analiz listesi aynı değil. Resmi cetvelle karşılaştırıp kullanılacak listeyi seçin. Seçim uygunluk onayı değildir; ardından kalem, fiyat ve belge kontrollerini tamamlayın.</p>
|
||||
<label className="block text-xs">Teklifte kullanılacak kalem listesi
|
||||
<select value={sourceOptions.selected} onChange={e => {
|
||||
setItemSource({source:e.target.value,signature:sourceOptions.signature});
|
||||
setReviewedSource(null); setDirty(true);
|
||||
}} className="block mt-1 rounded border p-2 w-full">
|
||||
<option value="">Resmi cetveli kontrol ederek seçin</option>
|
||||
<option value="tender">İhale havuzu — {sourceOptions.tenderCount} kalem</option>
|
||||
<option value="analysis">Doküman analizi — {sourceOptions.analysisCount} kalem</option>
|
||||
</select>
|
||||
</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>}
|
||||
{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 && (
|
||||
|
||||
Reference in New Issue
Block a user