feat: track document and health product preparation checks
This commit is contained in:
@@ -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.
|
- 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.
|
- Üç 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
|
## 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.
|
`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.
|
`node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { bidRowTotal, validateBidRows, roundMoney } from '../services/bidValidation';
|
import { bidRowTotal, validateBidRows, roundMoney } from '../services/bidValidation';
|
||||||
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
|
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
|
||||||
|
import { PreparationChecks } from './PreparationChecks';
|
||||||
|
import { preparationBlockers, documentKey } from '../services/preparationChecks';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import {
|
import {
|
||||||
FolderCheck, ChevronDown, ChevronUp, FileSignature, Table2, ListChecks,
|
FolderCheck, ChevronDown, ChevronUp, FileSignature, Table2, ListChecks,
|
||||||
@@ -59,8 +61,12 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
|||||||
const [kisim, setKisim] = useState(storedDraft.kisim || '');
|
const [kisim, setKisim] = useState(storedDraft.kisim || '');
|
||||||
const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || '');
|
const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || '');
|
||||||
|
|
||||||
const kalemler = sourceRows(tender, results);
|
const effectiveResults = results || storedDraft.analysisSource || {};
|
||||||
const sourceSnapshot = bidSourceSnapshot(tender, results, kalemler);
|
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 [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null);
|
||||||
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
|
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
|
||||||
const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot;
|
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 kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]);
|
||||||
const saveDraft = async () => {
|
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); }
|
if (saved) { setDirty(false); setHata(null); }
|
||||||
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
|
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
|
||||||
};
|
};
|
||||||
@@ -90,7 +96,8 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
|||||||
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
||||||
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
|
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
|
||||||
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
|
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 = () => ({
|
const formVerisi = () => ({
|
||||||
...firma,
|
...firma,
|
||||||
@@ -110,7 +117,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
|||||||
const f = formVerisi();
|
const f = formVerisi();
|
||||||
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam));
|
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam));
|
||||||
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, 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) {
|
} catch (e) {
|
||||||
setHata(e.message);
|
setHata(e.message);
|
||||||
}
|
}
|
||||||
@@ -297,6 +304,13 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<PreparationChecks rows={kalemler} documents={documents} manualDocuments={manualDocuments}
|
||||||
|
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks}/>
|
||||||
|
{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>
|
||||||
|
</details>}
|
||||||
|
|
||||||
{/* Toplam */}
|
{/* Toplam */}
|
||||||
<div className="bg-slate-900 text-white rounded-xl p-4 space-y-1">
|
<div className="bg-slate-900 text-white rounded-xl p-4 space-y-1">
|
||||||
<div className="flex items-baseline justify-between gap-3 flex-wrap">
|
<div className="flex items-baseline justify-between gap-3 flex-wrap">
|
||||||
@@ -327,7 +341,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
|||||||
{[
|
{[
|
||||||
{ t: 'mektup', ad: 'Teklif Mektubu', ikon: FileSignature, aciklama: 'Rakam ve yazı ile bedel' },
|
{ 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: '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) => (
|
].map((b) => (
|
||||||
<button
|
<button
|
||||||
key={b.t}
|
key={b.t}
|
||||||
@@ -349,7 +363,7 @@ export const BidPackageBuilder = ({ results, tender }) => {
|
|||||||
<p className="text-[11px] text-slate-500">
|
<p className="text-[11px] text-slate-500">
|
||||||
{eksikler.length > 0
|
{eksikler.length > 0
|
||||||
? 'Yukarıdaki zorunlu alanları doldurun.'
|
? '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.'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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}) => <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}) {
|
||||||
|
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 <section className="space-y-4" aria-label="Belge ve ürün uygunluğu">
|
||||||
|
<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">
|
||||||
|
{documents.map(d => {
|
||||||
|
const key = documentKey(d), c = checks.documents?.[key] || {};
|
||||||
|
return <fieldset key={key} className="border rounded-xl p-3 space-y-2">
|
||||||
|
<legend className="text-xs font-bold">{d.document} {d.mandatory === false ? '(isteğe bağlı)' : '(zorunlu)'}</legend>
|
||||||
|
<p className="text-xs text-slate-600">{d.note}</p>
|
||||||
|
{manualDocuments.some(item => documentKey(item) === key) && <button type="button" className="text-xs text-red-700"
|
||||||
|
onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>}
|
||||||
|
<Status value={c.status} optional={d.mandatory === false} onChange={status => updateDocument(key,{status})}/>
|
||||||
|
<label className="block text-xs">Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
|
||||||
|
<input className={fieldClass} value={c.evidence || ''} onChange={e => updateDocument(key,{evidence:e.target.value})}/>
|
||||||
|
</label>
|
||||||
|
</fieldset>;
|
||||||
|
})}
|
||||||
|
<div className="border border-dashed rounded-xl p-3 space-y-2">
|
||||||
|
<p className="text-xs font-semibold">Resmi dokümandan eksik belge gereksinimi ekle</p>
|
||||||
|
<label className="block text-xs">Belge adı<input className={fieldClass} value={name} onChange={e=>setName(e.target.value)}/></label>
|
||||||
|
<label className="block text-xs">Kaynak doküman ve madde<input className={fieldClass} value={clause} onChange={e=>setClause(e.target.value)}/></label>
|
||||||
|
<button type="button" className="text-xs font-bold text-blue-700 disabled:opacity-40" disabled={!name.trim() || !clause.trim()}
|
||||||
|
onClick={()=>{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</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{rows.map(row => {
|
||||||
|
const c=checks.products?.[row.priceKey] || {};
|
||||||
|
return <details key={row.priceKey} className="border rounded-xl p-3">
|
||||||
|
<summary className="cursor-pointer text-xs font-bold">{row.name} — ürün/hizmet ve yükümlülük kontrolü</summary>
|
||||||
|
<div className="space-y-2 mt-3">
|
||||||
|
<label className="block text-xs">Teklif edilen marka/model veya hizmet kapsamı<input className={fieldClass} value={c.product || ''} onChange={e=>updateProduct(row.priceKey,{product:e.target.value})}/></label>
|
||||||
|
<label className="block text-xs">Teknik doküman/madde ve uygunluk dayanağı<input className={fieldClass} value={c.source || ''} onChange={e=>updateProduct(row.priceKey,{source:e.target.value})}/></label>
|
||||||
|
<label className="block text-xs">Ürün/hizmet uygunluğu<select className={fieldClass} value={c.status || 'pending'} onChange={e=>updateProduct(row.priceKey,{status:e.target.value})}>
|
||||||
|
<option value="pending">Kontrol gerekli</option><option value="suitable">Uygun</option><option value="unsuitable">Uygun değil</option>
|
||||||
|
</select></label>
|
||||||
|
{HEALTH_CHECKS.map(([key,label])=><fieldset key={key} className="bg-slate-50 rounded-lg p-2 space-y-1">
|
||||||
|
<legend className="text-xs font-semibold">{label}</legend>
|
||||||
|
<Status value={c[key]?.status} onChange={status=>updateProduct(row.priceKey,{[key]:{...c[key],status}})}/>
|
||||||
|
<label className="block text-xs">Kaynak, karşılanan koşul veya uygulanmama gerekçesi<input className={fieldClass} value={c[key]?.evidence || ''} onChange={e=>updateProduct(row.priceKey,{[key]:{...c[key],evidence:e.target.value}})}/></label>
|
||||||
|
</fieldset>)}
|
||||||
|
<label className="block text-xs">Sorumlu<input className={fieldClass} value={c.owner || ''} onChange={e=>updateProduct(row.priceKey,{owner:e.target.value})}/></label>
|
||||||
|
</div>
|
||||||
|
</details>;
|
||||||
|
})}
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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});});
|
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()=>{
|
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);
|
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};
|
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);
|
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);
|
const read=await request('/api/me/tender-state');assert.deepEqual(read.data.records[data.key].data.bidDraft,draft);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user