fix: bind bid prices and review to source content

This commit is contained in:
2026-09-06 08:41:14 +03:00
parent 456728b4bd
commit 465874ebbb
5 changed files with 91 additions and 12 deletions
+26 -10
View File
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react';
import { bidRowTotal, validateBidRows, roundMoney } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
import { useApp } from '../context/AppContext';
import {
FolderCheck, ChevronDown, ChevronUp, FileSignature, Table2, ListChecks,
@@ -58,10 +59,12 @@ export const BidPackageBuilder = ({ results, tender }) => {
const [kisim, setKisim] = useState(storedDraft.kisim || '');
const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || '');
const kalemler = tender?.items?.length
? tender.items.map(k => ({name:k.ad,quantity:`${k.miktar} ${k.birim || ''}`,keySpecs:[]}))
: (Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : []);
const [fiyatlar, setFiyatlar] = useState(storedDraft.fiyatlar || {});
const kalemler = sourceRows(tender, results);
const sourceSnapshot = bidSourceSnapshot(tender, results, kalemler);
const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null);
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot;
const eskiFiyatlar = storedDraft.priceSchema !== 1 && Object.keys(storedDraft.fiyatlar || {}).length > 0;
const alanYaz = (k, v) => {
const yeni = { ...firma, [k]: v };
@@ -70,13 +73,13 @@ export const BidPackageBuilder = ({ results, tender }) => {
};
const satirlar = useMemo(
() => kalemler.map((k, i) => ({ ...k, birimFiyat: fiyatlar[i] })),
() => kalemler.map(k => ({ ...k, birimFiyat: fiyatlar[k.priceKey] })),
[kalemler, fiyatlar],
);
const kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]);
const saveDraft = async () => {
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar}});
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource}});
if (saved) { setDirty(false); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
};
@@ -87,7 +90,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
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 hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik;
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli;
const formVerisi = () => ({
...firma,
@@ -142,6 +145,19 @@ export const BidPackageBuilder = ({ results, tender }) => {
<span role="status" className="text-xs">{dirty ? 'Kaydedilmemiş değişiklikler var' : storedDraft.firma ? 'Sunucuda kayıtlı taslak' : 'Henüz kaydedilmedi'}</span>
</div>
<div className="rounded-xl border border-amber-300 bg-amber-50 p-3 space-y-2">
{kaynakKontrolGerekli && <p role="alert" className="text-xs text-amber-900">
{reviewedSource ? 'İlan, kalem veya belge bilgisi değişti. Önceki kontrol bu sürüm için geçerli değil.' : 'Kalemler ve teklif koşulları resmi dokümanla henüz karşılaştırılmadı.'}
{' '}Kaynağı kontrol etmeden teklif çıktısı oluşturulamaz.
</p>}
{eskiFiyatlar && <p className="text-xs text-amber-900">Eski taslak fiyatları satır sırasına göre kaydedilmiş. Yanlış ürüne aktarılmaması için fiyatları yeniden girin.</p>}
<label className="flex items-start gap-2 text-xs text-slate-800">
<input type="checkbox" checked={!kaynakKontrolGerekli}
onChange={e => setReviewedSource(e.target.checked ? sourceSnapshot : null)} />
Bu sürümün kalemlerini, miktarlarını, teklif türünü ve geçerlilik süresini resmi dokümanla karşılaştırdım.
</label>
</div>
{/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && (
<div className="bg-amber-50 border border-amber-300 rounded-xl p-3.5 flex gap-2.5">
@@ -245,16 +261,16 @@ export const BidPackageBuilder = ({ results, tender }) => {
const { miktar, birim } = miktarAyristir(r.quantity);
const tutar = bidRowTotal(r);
return (
<tr key={i} className="border-b border-slate-100">
<tr key={r.priceKey} className="border-b border-slate-100">
<td className="py-1.5 pr-2 text-slate-800">{r.name || '—'}</td>
<td className="py-1.5 text-right text-slate-600 whitespace-nowrap">
{miktar ?? '—'} {birim}
</td>
<td className="py-1.5 text-right">
<input
value={fiyatlar[i] ?? ''}
value={fiyatlar[r.priceKey] ?? ''}
onChange={(e) =>
setFiyatlar((o) => ({ ...o, [i]: e.target.value.replace(/[^\d.]/g, '') }))}
setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value.replace(/[^\d.]/g, '') }))}
placeholder={r.estimatedUnitPrice != null ? String(r.estimatedUnitPrice) : '0'}
className="w-28 text-xs text-right px-2 py-1 rounded border border-slate-300"
/>
+1 -1
View File
@@ -323,7 +323,7 @@ export const DeepAnalysisReport = ({ results, tender }) => {
)}
{/* Analiz ve yol haritası okunduktan sonraki adım: teklif zarfını hazırla */}
<BidPackageBuilder results={results} tender={tender} />
<BidPackageBuilder key={tender?.ikn || tender?.id} results={results} tender={tender} />
</div>
);
+31
View File
@@ -0,0 +1,31 @@
// Prices belong to a specific source row, never to its current array position.
export function sourceRows(tender, results) {
const items = tender?.items?.length
? tender.items.map(k => ({ ...k, name: k.ad, quantity: `${k.miktar} ${k.birim || ''}`, keySpecs: k.keySpecs || [] }))
: (Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : []);
const occurrences = new Map();
return items.map(k => {
const identity = JSON.stringify([k.id ?? k.no ?? null, k.kisim ?? null, k.name, k.quantity, k.keySpecs || []]);
const occurrence = occurrences.get(identity) || 0;
occurrences.set(identity, occurrence + 1);
return { ...k, priceKey: JSON.stringify([identity, occurrence]) };
});
}
// Exact canonical content, not a lossy hash. Reordering distinct rows is harmless;
// a changed quantity, specification, announcement or document requirement is not.
export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, results)) {
return JSON.stringify({
tender: tender?.ikn || tender?.id,
announcement: tender?.aciklama || '',
deadline: [tender?.tarih || '', tender?.saat || ''],
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
rows: rows.map(r => r.priceKey).sort(),
documents: results?.documentChecklist || [],
});
}
export function restoreBidPrices(draft = {}) {
// Old positional prices cannot safely be assigned without their original rows.
return draft.priceSchema === 1 && draft.pricesBySource ? draft.pricesBySource : {};
}