@@ -327,7 +341,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
{[
{ t: 'mektup', ad: 'Teklif Mektubu', ikon: FileSignature, aciklama: 'Rakam ve yazı ile bedel' },
{ t: 'cetvel', ad: 'Birim Fiyat Teklif Cetveli', ikon: Table2, aciklama: `${kalemler.length} kalem` },
- { t: 'liste', ad: 'Evrak Kontrol Listesi', ikon: ListChecks, aciklama: `${(results?.documentChecklist || []).length} belge` },
+ { t: 'liste', ad: 'Evrak Kontrol Listesi', ikon: ListChecks, aciklama: `${documents.length} belge` },
].map((b) => (
{
{eksikler.length > 0
? 'Yukarıdaki zorunlu alanları doldurun.'
- : 'Belgeleri hazırlamak için teklif tutarını girin.'}
+ : 'Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.'}
)}
diff --git a/src/components/PreparationChecks.jsx b/src/components/PreparationChecks.jsx
new file mode 100644
index 0000000..ebbf7ff
--- /dev/null
+++ b/src/components/PreparationChecks.jsx
@@ -0,0 +1,58 @@
+import React, {useState} from 'react';
+import {HEALTH_CHECKS, documentKey} 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}) =>
onChange(e.target.value)}>
+ Kontrol gerekli Doğrulandı
+ {optional && Uygulanmıyor — gerekçe gerekli }
+ ;
+export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks}) {
+ const [name,setName] = useState('');
+ const [clause,setClause] = useState('');
+ const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}}));
+ const updateProduct = (key, patch) => setChecks(old => ({...old,products:{...old.products,[key]:{...old.products?.[key],...patch}}}));
+ return
+ 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.
+
+ {documents.map(d => {
+ const key = documentKey(d), c = checks.documents?.[key] || {};
+ return
+ {d.document} {d.mandatory === false ? '(isteğe bağlı)' : '(zorunlu)'}
+ {d.note}
+ {manualDocuments.some(item => documentKey(item) === key) && setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır }
+ updateDocument(key,{status})}/>
+ Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
+ updateDocument(key,{evidence:e.target.value})}/>
+
+ ;
+ })}
+
+
Resmi dokümandan eksik belge gereksinimi ekle
+
Belge adı setName(e.target.value)}/>
+
Kaynak doküman ve madde setClause(e.target.value)}/>
+
{const d={document:name.trim(),note:clause.trim(),mandatory:true};if(!documents.some(x=>documentKey(x)===documentKey(d)))setManualDocuments([...manualDocuments,d]);setName('');setClause('');}}>Belge gereksinimi ekle
+
+
+ {rows.map(row => {
+ const c=checks.products?.[row.priceKey] || {};
+ return
+ {row.name} — ürün/hizmet ve yükümlülük kontrolü
+
+ Teklif edilen marka/model veya hizmet kapsamı updateProduct(row.priceKey,{product:e.target.value})}/>
+ Teknik doküman/madde ve uygunluk dayanağı updateProduct(row.priceKey,{source:e.target.value})}/>
+ Ürün/hizmet uygunluğuupdateProduct(row.priceKey,{status:e.target.value})}>
+ Kontrol gerekli Uygun Uygun değil
+
+ {HEALTH_CHECKS.map(([key,label])=>
+ {label}
+ updateProduct(row.priceKey,{[key]:{...c[key],status}})}/>
+ Kaynak, karşılanan koşul veya uygulanmama gerekçesi updateProduct(row.priceKey,{[key]:{...c[key],evidence:e.target.value}})}/>
+ )}
+ Sorumlu updateProduct(row.priceKey,{owner:e.target.value})}/>
+
+ ;
+ })}
+ ;
+}
diff --git a/src/services/preparationChecks.js b/src/services/preparationChecks.js
new file mode 100644
index 0000000..e49eaab
--- /dev/null
+++ b/src/services/preparationChecks.js
@@ -0,0 +1,27 @@
+export const HEALTH_CHECKS = [
+ ['uts', 'ÜTS / ürün kaydı'],
+ ['sample', 'Numune / demonstrasyon'],
+ ['service', 'Kurulum / bakım / servis'],
+ ['delivery', 'Teslimat ve süre'],
+];
+export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']);
+export function preparationBlockers(rows, documents, checks = {}) {
+ const errors = [];
+ if (!documents.length) errors.push('Belge gereksinimleri belirlenmedi. Resmi dokümana göre belge ekleyin.');
+ for (const d of documents) {
+ const c = checks.documents?.[documentKey(d)] || {};
+ if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue;
+ if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`);
+ }
+ for (const row of rows) {
+ const c = checks.products?.[row.priceKey] || {};
+ if (c.status !== 'suitable' || !c.product?.trim() || !c.source?.trim())
+ errors.push(`${row.name}: ürün/hizmet uygunluğu ve teknik kaynak eksik veya uygun değil.`);
+ for (const [key, label] of HEALTH_CHECKS) {
+ const field = c[key] || {};
+ if (!['verified','notApplicable'].includes(field.status) || !field.evidence?.trim())
+ errors.push(`${row.name}: ${label} kontrolü eksik.`);
+ }
+ }
+ return errors;
+}
diff --git a/tests/http-state.test.mjs b/tests/http-state.test.mjs
index 4f71ede..cd3a66c 100644
--- a/tests/http-state.test.mjs
+++ b/tests/http-state.test.mjs
@@ -26,7 +26,7 @@ before(async()=>{
after(async()=>{if(child){child.kill();await new Promise(r=>child.once('exit',r));}rmSync(dir,{recursive:true,force:true});});
test('HTTP preparation requires auth, persists draft and rejects stale revisions',async()=>{
assert.equal((await request('/api/me/tender-state','GET',null,null)).status,401);
- const draft={firma:{firmaUnvan:'QA Laboratuvar'},fiyatlar:{0:'12.50'},gecerlilikGun:'120'};
+ const draft={firma:{firmaUnvan:'QA Laboratuvar'},priceSchema:1,pricesBySource:{kit1:'12.50'},gecerlilikGun:'120',checks:{products:{kit1:{product:'Test kit',status:'suitable',source:'Teknik 3.2'}},documents:{doc1:{status:'verified',evidence:'Test v1'}}},manualDocuments:[{document:'Yetki',note:'7.1',mandatory:true}],analysisSource:{itemsAndSpecs:[{name:'Kit',quantity:'2 adet'}],documentChecklist:[]}};
const data={key:'2026/1582991',patch:{takipte:true,gorev:{kontrolListesi:[]},bidDraft:draft},snapshot:{id:'ilan-qa',ikn:'2026/1582991'},version:0};
assert.equal((await request('/api/me/tender-state','PUT',data)).status,200);
const read=await request('/api/me/tender-state');assert.deepEqual(read.data.records[data.key].data.bidDraft,draft);
diff --git a/tests/preparation-checks.test.mjs b/tests/preparation-checks.test.mjs
new file mode 100644
index 0000000..261c57d
--- /dev/null
+++ b/tests/preparation-checks.test.mjs
@@ -0,0 +1,19 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {preparationBlockers,documentKey,HEALTH_CHECKS} from '../src/services/preparationChecks.js';
+const rows=[{priceKey:'kit1',name:'Kit'}];
+const docs=[{document:'Yetki belgesi',mandatory:true,note:'Şartname 7.1'}];
+const complete=()=>({documents:{[documentKey(docs[0])]:{status:'verified',evidence:'Yetki v2, süre kontrol edildi, 7.1'}},products:{kit1:{product:'QA model',source:'Teknik 3.2, katalog s4',status:'suitable',...Object.fromEntries(HEALTH_CHECKS.map(([key])=>[key,{status:'notApplicable',evidence:'Doküman ilgili maddesi kapsamında uygulanmıyor'}]))}}});
+test('priced rows without document and health review remain incomplete',()=>{
+ assert.ok(preparationBlockers(rows,[],{}).length>=6);
+ assert.equal(preparationBlockers(rows,docs,complete()).length,0);
+});
+test('unsuitable product, missing evidence and mandatory document exemption block readiness',()=>{
+ const c=complete();c.products.kit1.status='unsuitable';assert.ok(preparationBlockers(rows,docs,c).some(x=>x.includes('uygun değil')));
+ c.products.kit1.status='suitable';c.products.kit1.uts.evidence=' ';assert.ok(preparationBlockers(rows,docs,c).some(x=>x.includes('ÜTS')));
+ c.documents[documentKey(docs[0])].status='notApplicable';assert.ok(preparationBlockers(rows,docs,c).some(x=>x.includes('Yetki')));
+});
+test('changed requirement and changed product row require new checks',()=>{
+ assert.ok(preparationBlockers(rows,[{...docs[0],note:'Zeyilname 2'}],complete()).length>0);
+ assert.ok(preparationBlockers([{...rows[0],priceKey:'kit2'}],docs,complete()).length>0);
+});