From e4b02f5dd3abfb7dd5e660527a50c75e4965521e Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 08:42:59 +0300 Subject: [PATCH] feat: track document and health product preparation checks --- docs/quality/uygulama-durumu.md | 10 ++++- src/components/BidPackageBuilder.jsx | 28 ++++++++++---- src/components/PreparationChecks.jsx | 58 ++++++++++++++++++++++++++++ src/services/preparationChecks.js | 27 +++++++++++++ tests/http-state.test.mjs | 2 +- tests/preparation-checks.test.mjs | 19 +++++++++ 6 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 src/components/PreparationChecks.jsx create mode 100644 src/services/preparationChecks.js create mode 100644 tests/preparation-checks.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index d71a4f3..681620f 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -34,9 +34,17 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo - Eski konumsal fiyatlar kaynak satırları olmadan güvenle eşlenemediği için otomatik taşınmaz; kullanıcıya yeniden giriş gerektiği gösterilir. Bu davranış yeni taslak kaydında da korunur. - Üç regresyon testi sıralama, içerik değişikliği ve eski taslak senaryolarını doğrular. Henüz gerçek belge yükleme/zeyilname ve tarayıcı form kabulü tamamlanmadı. Bu kontrol tüm belge/AI kaynak sürüm modelinin yerine geçmez. +## Belge ve sağlık ürünü kontrolleri + +- Hazırlık formunda kaynak maddeli belge gereksinimi ekleme/kaldırma, belge adı/sürümü ve geçerlilik kontrolü kaydı eklendi. Zorunlu belgeye uygulanmıyor denilerek çıktı engeli aşılamaz. +- Kalem bazında ürün/model veya hizmet kapsamı, teknik uygunluk kaynağı, sorumlu, ÜTS, numune/demonstrasyon, servis/kurulum ve teslimat kontrolleri bulunur. Uygulanmıyor seçimi gerekçe ister; uygun olmayan ürün çıktıyı engeller. +- Eksik kontrollerin listesi fiyatlandırma ekranında görünür. Bu alanlar kullanıcı beyanı/kontrol kaydıdır; belge deposundaki gerçek dosyaya otomatik bağlantı ve resmi yeterlik doğrulaması henüz yoktur. +- Kontroller, elle eklenen belgeler ve analizden gelen kalem/belge listesi taslakla saklanır. HTTP testi bu alanların hesap içinde yazılıp geri okunmasını doğrular. Üç kural testi eksik kaynak, uygun olmayan ürün, zorunlu belgenin atlanması ve değişmiş gereksinimleri kapsar. +- Tarayıcıda tüm bu alanları doldurma, yenileme ve çok kalemli kullanılabilirlik kabulü bekliyor. AI raporundaki eski yerel checklist işaretleri henüz bu kalıcı kontrol modeline bağlanmadı. D06/D07 tamamlanmış sayılmaz. + ## Doğrulama -`npm test`: 22/22 geçti (son yeniden çalıştırma: 6 Eylül 2026). +`npm test`: 25/25 geçti (son yeniden çalıştırma: 6 Eylül 2026). `npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor. `node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 0e1a5f9..794a774 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -1,6 +1,8 @@ import React, { useState, useMemo } from 'react'; import { bidRowTotal, validateBidRows, roundMoney } from '../services/bidValidation'; import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource'; +import { PreparationChecks } from './PreparationChecks'; +import { preparationBlockers, documentKey } from '../services/preparationChecks'; import { useApp } from '../context/AppContext'; import { FolderCheck, ChevronDown, ChevronUp, FileSignature, Table2, ListChecks, @@ -59,8 +61,12 @@ export const BidPackageBuilder = ({ results, tender }) => { const [kisim, setKisim] = useState(storedDraft.kisim || ''); const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || ''); - const kalemler = sourceRows(tender, results); - const sourceSnapshot = bidSourceSnapshot(tender, results, kalemler); + const effectiveResults = results || storedDraft.analysisSource || {}; + const [manualDocuments, setManualDocuments] = useState(storedDraft.manualDocuments || []); + const [checks, setChecks] = useState(storedDraft.checks || {}); + const documents = [...new Map([...(effectiveResults.documentChecklist || []), ...manualDocuments].map(d => [documentKey(d),d])).values()]; + const kalemler = sourceRows(tender, effectiveResults); + const sourceSnapshot = bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler); const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null); const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft)); const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot; @@ -79,7 +85,7 @@ export const BidPackageBuilder = ({ results, tender }) => { 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,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource}}); + const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}}); if (saved) { setDirty(false); setHata(null); } else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); }; @@ -90,7 +96,8 @@ 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 && !kaynakKontrolGerekli; + const kontrolEksikleri = preparationBlockers(kalemler, documents, checks); + const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0; const formVerisi = () => ({ ...firma, @@ -110,7 +117,7 @@ export const BidPackageBuilder = ({ results, tender }) => { const f = formVerisi(); if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam)); if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam)); - if (tip === 'liste') belgeYazdir('Evrak Kontrol Listesi', evrakListesiHtml(f, results?.documentChecklist || [])); + if (tip === 'liste') belgeYazdir('Evrak Kontrol Listesi', evrakListesiHtml(f, documents)); } catch (e) { setHata(e.message); } @@ -297,6 +304,13 @@ export const BidPackageBuilder = ({ results, tender }) => {

)} + {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks}/> + {kontrolEksikleri.length > 0 &&
+ {kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı +
    {kontrolEksikleri.map((message,i)=>
  • {message}
  • )}
+
} + {/* Toplam */}
@@ -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) => (
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}) => ; +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) && } + updateDocument(key,{status})}/> + +
; + })} +
+

Resmi dokümandan eksik 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ü +
+ + + + {HEALTH_CHECKS.map(([key,label])=>
+ {label} + updateProduct(row.priceKey,{[key]:{...c[key],status}})}/> + +
)} + +
+
; + })} +
; +} 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); +});