Restrict preparation document links to their tender scope

This commit is contained in:
2026-09-06 10:17:14 +03:00
parent 91451b1134
commit 2b9e1953e4
5 changed files with 51 additions and 12 deletions
+3 -3
View File
@@ -127,7 +127,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
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 kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
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;
@@ -157,7 +157,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form, kaynak veya oturum değişti. Güncel bilgilerle yeniden çıktı oluşturun.');
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender)};
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
setVault(currentVault);
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
if (fileErrors.length) throw new Error(fileErrors.join(' '));
@@ -415,7 +415,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
)}
<PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks} vault={vault} refreshVault={()=>setVaultRefresh(n=>n+1)} downloadFile={(file)=>downloadDocument(file.id,file.ad).catch(e=>setHata(e.message))}/>
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks} vault={{...vault,tenderIkn:tender.ikn}} refreshVault={()=>setVaultRefresh(n=>n+1)} downloadFile={(file)=>downloadDocument(file.id,file.ad).catch(e=>setHata(e.message))}/>
{kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3">
<summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary>
<ul className="text-xs list-disc pl-5 mt-2 space-y-1">{kontrolEksikleri.map((message,i)=><li key={i}>{message}</li>)}</ul>
+6 -5
View File
@@ -1,11 +1,12 @@
import React, {useState} from 'react';
import {HEALTH_CHECKS, documentKey, documentRevision} from '../services/preparationChecks';
import {HEALTH_CHECKS, documentKey, documentRevision, documentScopeProblem} from '../services/preparationChecks';
const fieldClass = 'w-full border border-slate-300 rounded-lg p-2 text-xs bg-white';
const Status = ({value, onChange, optional = true}) => <select aria-label="Kontrol durumu" className={fieldClass} value={value || 'pending'} onChange={e => onChange(e.target.value)}>
<option value="pending">Kontrol gerekli</option><option value="verified">Doğrulandı</option>
{optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>}
</select>;
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks, vault, refreshVault, downloadFile}) {
const eligibleFiles = vault.documents.filter(file => !documentScopeProblem(file,vault.tenderIkn));
const [name,setName] = useState('');
const [clause,setClause] = useState('');
const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}}));
@@ -14,7 +15,7 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
<h5 className="font-extrabold text-sm">Belge ve ürün uygunluğu</h5>
<p className="text-xs text-slate-600">Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.</p>
<div className="space-y-2">
<p className="text-xs">Dosyaları Doküman Deposu'na yükleyin, ardından burada ilgili belge gereksinimine bağlayın.</p>
<p className="text-xs">Firma belgelerini veya bu İKN için yüklenmiş ihale dokümanlarını ilgili gereksinime bağlayın. Başka ihale dokümanları seçilemez.</p>
<button type="button" onClick={refreshVault} disabled={vault.loading} className="text-xs font-bold text-blue-700">Belge deposunu yenile</button>
{vault.loading && <p role="status" className="text-xs">Belge deposu yükleniyor…</p>}
{vault.error && <p role="alert" className="text-xs text-red-700">Belge deposu yüklenemedi: {vault.error}</p>}
@@ -27,12 +28,12 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>}
<label className="block text-xs">Bağlı dosya
<select className={fieldClass} value={c.documentId || ''} disabled={vault.loading || Boolean(vault.error)} onChange={e=>{
const file=vault.documents.find(f=>f.id===e.target.value);
const file=eligibleFiles.find(f=>f.id===e.target.value);
updateDocument(key,{documentId:file?.id || null,documentRevision:file ? documentRevision(file) : null,status:'pending'});
}}>
<option value="">Dosya seçin</option>
{c.documentId && !vault.documents.some(f=>f.id===c.documentId) && <option value={c.documentId}>Önceki dosya bulunamadı</option>}
{vault.documents.map(file=><option key={file.id} value={file.id}>{file.ad} · {file.son_kullanma || 'Süre bilgisi yok'}{file.erisilebilir === false ? ' · erişilemiyor' : ''}</option>)}
{c.documentId && !eligibleFiles.some(f=>f.id===c.documentId) && <option value={c.documentId}>Önceki dosya bulunamadı veya bu ihale için kullanılamıyor</option>}
{eligibleFiles.map(file=><option key={file.id} value={file.id}>{file.ad} · {file.son_kullanma || 'Süre bilgisi yok'}{file.erisilebilir === false ? ' · erişilemiyor' : ''}</option>)}
</select>
</label>
{vault.documents.some(f=>f.id===c.documentId) && <button type="button" className="text-xs text-blue-700" onClick={()=>downloadFile(vault.documents.find(f=>f.id===c.documentId))}>Bağlı dosyayı indir</button>}