From 2b9e1953e4c22f656c4b3dddfaaa919f60d0ea35 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:17:14 +0300 Subject: [PATCH] Restrict preparation document links to their tender scope --- docs/quality/uygulama-durumu.md | 12 ++++++++++++ src/components/BidPackageBuilder.jsx | 6 +++--- src/components/PreparationChecks.jsx | 11 ++++++----- src/services/preparationChecks.js | 15 ++++++++++++--- tests/preparation-checks.test.mjs | 19 ++++++++++++++++++- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index f4fefa9..05c8672 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -491,3 +491,15 @@ Ortak havuz istemcisi HTTP hata/bozuk yanıtı boş liste gibi sunmaz. Panel hat Gerçek tarayıcı kabulü: localhost:5175 QA Giresun detayında “İhale kaydında aşağıda gösterilen 1 kalem var” mesajı ve “Malzeme / Kalem Listesi (1 Kalem)” tablosu birlikte doğrulandı. Kanıt: `qa-items-source-message.json`. Bu sentetik veriyle kaynak gösterimi testidir; resmi cetvel doğruluğu veya tüm kaynak uyuşmazlıklarının çözümü değildir. Ağ hatası/iptal/zaman aşımı ise otomatik testlerde sınandı. Tüm 106 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın tamamı ve canlı yayın açık. + +### D01 / D07 — Hazırlık belgelerinde ihale kapsamı + +Hazırlık belge seçicisi bütün depo kayıtlarını sunuyor; son kontrolde dosya erişimi, sürümü ve son kullanımı denetlenirken İKN/tür kapsamı kontrol edilmiyordu. Böylece aynı hesaba ait başka ihalenin dokümanı mevcut hazırlığa bağlanabiliyordu. + +Yeni kapsam denetimi firma belgelerini yeniden kullanılabilir tutar; `ihale` türündeki dosyalar yalnız mevcut İKN ile eşleşince kullanılabilir. Türü bilinmeyen veya ihale bağlantısı olmayan kayıtlar açık hata verir. Seçici uygun dosyaları listeler; önceden bağlanmış uygunsuz dosyayı görünür bir “kullanılamıyor” seçeneğiyle korur, sessizce başka dosya seçmez. Genel hazır olma kontrolü ve çıktı öncesi güncel envanter kontrolü aynı İKN bağlamını kullanır. + +Belge kontrol sürümüne tür ve İKN eklendi. Bu alanların değişmesi yeniden inceleme gerektirir; eski sürüm biçimiyle kaydedilmiş bağlantılar da güvenli biçimde yeniden seçim/kontrol ister. Bu, belgelerin gerçek yeterlik içeriğini otomatik doğrulamaz. + +Testler: başka ihale belgesi reddi, aynı ihale kabulü, firma belgesinin yeniden kullanımı, tüm hazırlık kontrolüne kapsamın yansıması, tür/bağ değişiminde inceleme geçersizliği. Tarayıcıda yeni seçici kapsamı kabulü halen açık. + +Sonuç: Tüm 108 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın tümü bitmedi; canlı yayın yapılmadı. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 2821c91..26ce889 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -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 }) => { )} {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 &&
{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı
    {kontrolEksikleri.map((message,i)=>
  • {message}
  • )}
diff --git a/src/components/PreparationChecks.jsx b/src/components/PreparationChecks.jsx index f612574..1b9b7cf 100644 --- a/src/components/PreparationChecks.jsx +++ b/src/components/PreparationChecks.jsx @@ -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}) => ; 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
Belge ve ürün uygunluğu

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.

-

Dosyaları Doküman Deposu'na yükleyin, ardından burada ilgili belge gereksinimine bağlayın.

+

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.

{vault.loading &&

Belge deposu yükleniyor…

} {vault.error &&

Belge deposu yüklenemedi: {vault.error}

} @@ -27,12 +28,12 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır} {vault.documents.some(f=>f.id===c.documentId) && } diff --git a/src/services/preparationChecks.js b/src/services/preparationChecks.js index b170621..237de69 100644 --- a/src/services/preparationChecks.js +++ b/src/services/preparationChecks.js @@ -5,10 +5,19 @@ export const HEALTH_CHECKS = [ ['delivery', 'Teslimat ve süre'], ]; export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']); -export const documentRevision = file => JSON.stringify([file.id,file.olusturma,file.boyut,file.son_kullanma || null]); -export function documentLinkProblem(check, files, deadlineDate) { +export const documentRevision = file => JSON.stringify([file.id,file.olusturma,file.boyut,file.son_kullanma || null,file.tur || null,file.ikn || null]); +export function documentScopeProblem(file, tenderIkn) { + if (file.tur === 'firma') return null; + if (file.tur !== 'ihale') return 'Belge türü doğrulanamadı. Doküman Deposu kaydını kontrol edin.'; + if (!tenderIkn || !file.ikn || String(file.ikn).trim() !== String(tenderIkn).trim()) + return 'Bu ihale dokümanı mevcut ihaleye bağlı değil. Doğru İKN için yüklenmiş dosyayı seçin.'; + return null; +} +export function documentLinkProblem(check, files, deadlineDate, tenderIkn) { const file = files.find(f=>f.id===check?.documentId); if (!file || file.erisilebilir === false) return 'Bağlı dosya bulunamadı veya erişilemiyor.'; + const scopeProblem = documentScopeProblem(file,tenderIkn); + if (scopeProblem) return scopeProblem; if (check.documentRevision !== documentRevision(file)) return 'Belge bilgisi değişti; dosyayı yeniden seçip kontrol edin.'; if (file.son_kullanma && ((!/^\d{4}-\d{2}-\d{2}$/.test(file.son_kullanma) || Number.isNaN(Date.parse(file.son_kullanma+'T00:00:00Z')) || new Date(file.son_kullanma+'T00:00:00Z').toISOString().slice(0,10) !== file.son_kullanma) || !deadlineDate || file.son_kullanma < deadlineDate)) return 'Belge son teklif tarihinde geçerli değil veya tarih doğrulanamadı.'; @@ -21,7 +30,7 @@ export function preparationBlockers(rows, documents, checks = {}, vault) { const c = checks.documents?.[documentKey(d)] || {}; if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue; if (vault) { - const problem = vault.loading ? 'Belge deposu yükleniyor.' : vault.error ? 'Belge deposu doğrulanamadı.' : documentLinkProblem(c,vault.documents || [],vault.deadlineDate); + const problem = vault.loading ? 'Belge deposu yükleniyor.' : vault.error ? 'Belge deposu doğrulanamadı.' : documentLinkProblem(c,vault.documents || [],vault.deadlineDate,vault.tenderIkn); if(problem) errors.push(`${d.document}: ${problem}`); } if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`); diff --git a/tests/preparation-checks.test.mjs b/tests/preparation-checks.test.mjs index be1e933..fa825e3 100644 --- a/tests/preparation-checks.test.mjs +++ b/tests/preparation-checks.test.mjs @@ -19,7 +19,7 @@ test('changed requirement and changed product row require new checks',()=>{ }); test('linked file must exist, match reviewed metadata and remain valid at tender deadline',()=>{ - const file={id:'f1',olusturma:'2026-09-01',boyut:100,son_kullanma:'2026-09-10',erisilebilir:true}; + const file={id:'f1',tur:'firma',olusturma:'2026-09-01',boyut:100,son_kullanma:'2026-09-10',erisilebilir:true}; const c={documentId:file.id,documentRevision:documentRevision(file)}; assert.equal(documentLinkProblem(c,[file],'2026-09-10'),null); assert.ok(documentLinkProblem(c,[],'2026-09-10')); @@ -29,3 +29,20 @@ test('linked file must exist, match reviewed metadata and remain valid at tender const invalid={...file,son_kullanma:'2026-02-31'}; assert.ok(documentLinkProblem({...c,documentRevision:documentRevision(invalid)},[invalid],'2026-01-01')); }); + +test('another tender document cannot satisfy preparation but company documents remain reusable',()=>{ + const file={id:'doc1',tur:'ihale',ikn:'2026/111',olusturma:'2026-09-01',boyut:20}; + const check={documentId:file.id,documentRevision:documentRevision(file)}; + assert.match(documentLinkProblem(check,[file],'2026-09-10','2026/222'),/mevcut ihaleye bağlı değil/); + assert.equal(documentLinkProblem(check,[file],'2026-09-10','2026/111'),null); + const company={...file,tur:'firma',ikn:null}; + assert.equal(documentLinkProblem({...check,documentRevision:documentRevision(company)},[company],'2026-09-10','2026/222'),null); + const c=complete();c.documents[documentKey(docs[0])]={...c.documents[documentKey(docs[0])],...check}; + assert.ok(preparationBlockers(rows,docs,c,{documents:[file],deadlineDate:'2026-09-10',tenderIkn:'2026/222'}).some(e=>e.includes('mevcut ihaleye bağlı değil'))); +}); +test('classification or tender-link changes invalidate the previous review',()=>{ + const file={id:'doc1',tur:'firma',ikn:null,olusturma:'2026-09-01',boyut:20}; + const check={documentId:file.id,documentRevision:documentRevision(file)}; + assert.match(documentLinkProblem(check,[{...file,tur:'ihale',ikn:'2026/111'}],'2026-09-10','2026/111'),/yeniden seçip/); + assert.match(documentLinkProblem(check,[{...file,tur:undefined}],'2026-09-10','2026/111'),/Belge türü/); +});