Preserve preparation conditions and invalidate outdated source reviews

This commit is contained in:
2026-09-06 10:43:14 +03:00
parent fef6f2ab89
commit fa56e0fe83
5 changed files with 57 additions and 2 deletions
+13 -2
View File
@@ -1,3 +1,4 @@
import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
import { preparationDeadlineProblem } from '../services/preparationDeadline';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
@@ -116,7 +117,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,procurementTerms:effectiveResults.procurementTerms || 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:preparationAnalysisSource(effectiveResults)}});
if (!mounted.current) return;
if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
@@ -229,10 +230,20 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<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.
Bu sürümün kalemlerini, miktarlarını, teklif türünü, geçerlilik süresini ve aşağıdaki yeterlik/risk koşullarını resmi dokümanla karşılaştırdım.
</label>
</div>
{preparationConditionSections.some(([field]) => effectiveResults[field]?.length > 0) &&
<details className="rounded-xl border border-amber-300 p-3" open>
<summary className="font-bold text-sm cursor-pointer">Yeterlik, risk ve diğer kaynak koşulları</summary>
<p className="text-xs mt-2">Analiz bulgularını resmi maddelerle karşılaştırın. Bu liste bütün koşulların bulunduğunu veya karşılandığını kanıtlamaz.</p>
{preparationConditionSections.filter(([field]) => effectiveResults[field]?.length > 0).map(([field,label]) =>
<section key={field} className="mt-3 text-xs"><h5 className="font-bold">{label}</h5>
<ul className="list-disc pl-5 space-y-1">{effectiveResults[field].map((row,index)=><li key={index}>{conditionText(row)}</li>)}</ul>
</section>)}
</details>}
{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>
+2
View File
@@ -1,3 +1,4 @@
import { preparationAnalysisSource } from './preparationAnalysis.js';
// Prices belong to a specific source row, never to its current array position.
export function sourceRows(tender, results, preferredSource = 'tender') {
const items = preferredSource !== 'analysis' && tender?.items?.length
@@ -33,6 +34,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res
deadline: [tender?.tarih || '', tender?.saat || ''],
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
procurementTerms: results?.procurementTerms || null,
analysis: preparationAnalysisSource(results),
humanReview: results?.sourceReview?.humanReview || null,
analyzedItems: results?.itemsAndSpecs || [],
rows: rows.map(r => r.priceKey).sort(),
+21
View File
@@ -0,0 +1,21 @@
export const preparationConditionSections = [
['qualificationCriteria','Yeterlik koşulları'],
['criticalRequirements','Kritik şartlar'],
['risksAndPenalties','Riskler ve cezalar'],
['disqualificationRisks','Değerlendirme dışı kalma riskleri'],
['criticalDetails','Kontrol edilecek ayrıntılar'],
['deadlines','Süreler ve tarihler'],
['costDrivers','Maliyet yükümlülükleri'],
['questionsForAdministration','İdareye sorulacak sorular'],
];
export function preparationAnalysisSource(results = {}) {
const fields = ['itemsAndSpecs','documentChecklist','roadmap',...preparationConditionSections.map(([field])=>field)];
return Object.fromEntries([
...fields.map(field=>[field,results[field] || []]),
...['documentVersion','procurementTerms','sourceReview','sourceEvidence','citations','summary','scopeOfWork'].map(field=>[field,results[field] ?? null]),
]);
}
export function conditionText(row) {
if (typeof row === 'string') return row;
return ['title','label','value','clause','text','detail','where','whyItMatters','note'].map(key=>row?.[key]).filter(Boolean).join(' — ');
}