Add saved bid preparation overview with actionable missing checks
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-09-06",
|
||||||
|
"environment": "localhost:5182 five synthetic tenders, real UI/API save",
|
||||||
|
"checks": {
|
||||||
|
"fiveDossiers": true,
|
||||||
|
"allUnassignedVisible": true,
|
||||||
|
"directFieldFocus": true,
|
||||||
|
"savedFieldCleared": true,
|
||||||
|
"ownerUpdated": true,
|
||||||
|
"reloadPreserved": true
|
||||||
|
},
|
||||||
|
"limits": [
|
||||||
|
"Synthetic incomplete dossiers, not five official bid approvals",
|
||||||
|
"One direct field focus, saved edit and one owner assignment tested",
|
||||||
|
"Distinct owners and document revision errors covered by unit tests",
|
||||||
|
"Real PDF/official documents remain open"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -874,3 +874,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
|
|||||||
- Kayıt sırasında durum/atama/sürükleme kilitlenir; mevcut sunucu kayıt hatası panoda gösterilir. Durum iyimser olarak değiştirilmez, mevcut mutateTender kalıcılık akışı korunur.
|
- Kayıt sırasında durum/atama/sürükleme kilitlenir; mevcut sunucu kayıt hatası panoda gösterilir. Durum iyimser olarak değiştirilmez, mevcut mutateTender kalıcılık akışı korunur.
|
||||||
- Yerel 5182 sentetik Giresun görevinde 5 kontrol geçti: sahte düğmenin kalkması, desteklenmeme açıklaması, durum seçimi, yenileme sonrası durumun korunması ve erişilebilir kontrol adları. Kanıt qa-kanban-controls.json. Derleme geçti. Önceki 197 test tekrar çalıştırılmadı; sunucu değişmedi.
|
- Yerel 5182 sentetik Giresun görevinde 5 kontrol geçti: sahte düğmenin kalkması, desteklenmeme açıklaması, durum seçimi, yenileme sonrası durumun korunması ve erişilebilir kontrol adları. Kanıt qa-kanban-controls.json. Derleme geçti. Önceki 197 test tekrar çalıştırılmadı; sunucu değişmedi.
|
||||||
- Bu tur atama mutasyonu, hata enjeksiyonu, yalnız klavyeyle tam gezinme veya beş dosyalı eksik/sorumlu/sonraki işlem özetinin kabulünü kanıtlamaz. D07'nin ortak hazırlık özeti açık.
|
- Bu tur atama mutasyonu, hata enjeksiyonu, yalnız klavyeyle tam gezinme veya beş dosyalı eksik/sorumlu/sonraki işlem özetinin kabulünü kanıtlamaz. D07'nin ortak hazırlık özeti açık.
|
||||||
|
|
||||||
|
### D07 — Beş dosyalı hazırlık özeti — 6 Eylül 2026
|
||||||
|
- Kanban'a Hazırlık dosyaları özeti eklendi. Aynı takip/sorumlu filtresindeki tüm ihaleler için sorumlu, kayıtlı taslaktaki kontrol sayısı, belge eksikleri ve sıradaki kontrol bir tabloda gösterilir. Kaydedilmemiş form değişiklikleri özetin dışında olduğu açıklanır; temiz özet başvuru onayı olarak sunulmaz.
|
||||||
|
- preparationSummary mevcut kaynak/kalem/kısım, firma alanları, teklif türü/geçerliliği, fiyat, sağlık koşulları ve belge bağlantı/sürüm/süre kontrollerini kullanır. Zorunlu firma alanları ortak modüle taşındı. Belge deposu hata/yükleme durumları gösterilir; yenileme düğmesi vardır.
|
||||||
|
- Özet bağlantıları detayın doğrudan hazırlık sekmesini açar ve ilgili firma/fiyat/kontrol alanına odak taşır. Belge ve ürün eksikleri ilgili kontrol bölümüne gider. Çıktı öncesi canlı belge doğrulamaları korunur.
|
||||||
|
- 200 otomatik test ve derleme geçti. Beş ayrı sorumlu/belge içeren taslak özeti, eski belge sürümü/depo hatası ve geçmiş deadline önceliği test edildi.
|
||||||
|
- 5182 yerel tarayıcıda beş sentetik görev tek tabloda görüldü; vergi alanına doğrudan odak, kaydedilen düzeltmenin özetten kalkması, sorumlu atamasının yansıması ve yenileme sonrası kalıcılık dahil 6 kontrol geçti (qa-preparation-overview.json).
|
||||||
|
- Bu sentetik kabul gerçek beş resmi dosyanın uygunluğunu kanıtlamaz. Firma portföyü/ürün kapasitesi, tam mobil erişilebilirlik, her eksik belgeye ayrı alt-alan odağı ve resmi çıktılarla ilgili açık maddeler devam eder. Özet kontrolleri kayıtlı taslağa dayanır; yeni AI analizi veya kaydedilmemiş değişiklikler dosya ekranında ayrıca değerlendirilir.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { bidFirmFields as ALANLAR } from '../services/bidFirmFields';
|
||||||
import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
|
import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
|
||||||
import { preparationDeadlineProblem } from '../services/preparationDeadline';
|
import { preparationDeadlineProblem } from '../services/preparationDeadline';
|
||||||
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
|
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
|
||||||
@@ -29,23 +30,13 @@ import {
|
|||||||
* Firma bilgileri localStorage'da tutulur — her ihalede yeniden sorulmasın.
|
* Firma bilgileri localStorage'da tutulur — her ihalede yeniden sorulmasın.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ALANLAR = [
|
|
||||||
{ k: 'firmaUnvan', l: 'Firma ticaret unvanı', zorunlu: true, genis: true },
|
|
||||||
{ k: 'adres', l: 'Tebligata esas adres', zorunlu: true, genis: true },
|
|
||||||
{ k: 'vergiDairesi', l: 'Vergi dairesi', zorunlu: true },
|
|
||||||
{ k: 'vergiNo', l: 'Vergi numarası', zorunlu: true },
|
|
||||||
{ k: 'sicilNo', l: 'Ticaret sicil / MERSİS no', zorunlu: false },
|
|
||||||
{ k: 'yetkiliAd', l: 'Yetkili ad soyad', zorunlu: true },
|
|
||||||
{ k: 'yetkiliGorev', l: 'Yetkilinin görevi', zorunlu: true, ph: 'Örn. Genel Müdür' },
|
|
||||||
{ k: 'telefon', l: 'Telefon', zorunlu: true },
|
|
||||||
{ k: 'eposta', l: 'E-posta', zorunlu: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi';
|
const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi';
|
||||||
|
|
||||||
const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
|
const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
|
||||||
|
|
||||||
export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocus }) => {
|
||||||
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
|
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
|
||||||
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
|
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
|
||||||
const storedDraft = tender?.bidDraft || {};
|
const storedDraft = tender?.bidDraft || {};
|
||||||
@@ -67,6 +58,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
|||||||
target.scrollIntoView({block:'center'});
|
target.scrollIntoView({block:'center'});
|
||||||
target.focus({preventScroll:true});
|
target.focus({preventScroll:true});
|
||||||
};
|
};
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!initialFocus)return;
|
||||||
|
const frame=requestAnimationFrame(()=>goToMissing(initialFocus));
|
||||||
|
return ()=>cancelAnimationFrame(frame);
|
||||||
|
},[initialFocus]);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [acik, setAcik] = useState(true);
|
const [acik, setAcik] = useState(true);
|
||||||
const [hata, setHata] = useState(null);
|
const [hata, setHata] = useState(null);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PreparationOverview } from './PreparationOverview';
|
||||||
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
||||||
import { deadlineInfo } from '../services/tenderCalendar';
|
import { deadlineInfo } from '../services/tenderCalendar';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
@@ -91,6 +92,7 @@ export const KanbanBoard = () => {
|
|||||||
|
|
||||||
{savingCount > 0 && <p role="status" className="text-sm text-blue-800">Değişiklik kaydediliyor…</p>}
|
{savingCount > 0 && <p role="status" className="text-sm text-blue-800">Değişiklik kaydediliyor…</p>}
|
||||||
{saveError && <p role="alert" className="text-sm text-red-700">{saveError}</p>}
|
{saveError && <p role="alert" className="text-sm text-red-700">{saveError}</p>}
|
||||||
|
<PreparationOverview tenders={filteredTenders} now={now}/>
|
||||||
{/* Kanban Columns Grid */}
|
{/* Kanban Columns Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3.5 overflow-x-auto pb-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3.5 overflow-x-auto pb-4">
|
||||||
{columns.map(col => {
|
{columns.map(col => {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React,{useEffect,useState} from 'react';
|
||||||
|
import {listDocuments} from '../services/account';
|
||||||
|
import {preparationSummary} from '../services/preparationSummary';
|
||||||
|
import {useApp} from '../context/AppContext';
|
||||||
|
import {getToken} from '../services/auth';
|
||||||
|
|
||||||
|
export function PreparationOverview({tenders,now}) {
|
||||||
|
const {authUser}=useApp();
|
||||||
|
return <Overview key={`${authUser?.email}:${getToken()}`} tenders={tenders} now={now}/>;
|
||||||
|
}
|
||||||
|
function Overview({tenders,now}) {
|
||||||
|
const {setActiveTenderDetail}=useApp();
|
||||||
|
const [vault,setVault]=useState({loading:true,error:null,documents:[]}),[refresh,setRefresh]=useState(0);
|
||||||
|
useEffect(()=>{
|
||||||
|
let cancelled=false;
|
||||||
|
setVault({loading:true,error:null,documents:[]});
|
||||||
|
listDocuments().then(data=>{if(!cancelled)setVault({loading:false,error:null,documents:data.belgeler});})
|
||||||
|
.catch(error=>{if(!cancelled)setVault({loading:false,error:error.message,documents:[]});});
|
||||||
|
return()=>{cancelled=true;};
|
||||||
|
},[refresh]);
|
||||||
|
const open=(tender,target)=>setActiveTenderDetail({...tender,preparationTarget:target});
|
||||||
|
return <section aria-label="Hazırlık dosyaları özeti" className="bg-white border rounded-xl p-4 space-y-3">
|
||||||
|
<h2 className="font-bold text-lg">Hazırlık dosyaları · {tenders.length}</h2>
|
||||||
|
<p className="text-xs text-slate-600">Panodaki ve sorumlu filtresindeki kayıtlı taslaklar gösterilir. Kaydetmediğiniz değişiklikler bu özete girmez. Eksik sayısı başvuru onayı değildir; çıktı sırasında belgeler yeniden kontrol edilir.</p>
|
||||||
|
<button className="border rounded px-3 py-2 text-xs" disabled={vault.loading} onClick={()=>setRefresh(n=>n+1)}>Belge durumunu yenile</button>
|
||||||
|
{vault.loading&&<p role="status" className="text-xs">Belge durumu kontrol ediliyor…</p>}
|
||||||
|
{vault.error&&<p role="alert" className="text-xs text-red-700">Belge durumu doğrulanamadı: {vault.error}</p>}
|
||||||
|
{!tenders.length ? <p className="text-sm">İhaleyi takibe alın veya hazırlık görevi açın; dosyası burada görünsün.</p> :
|
||||||
|
<div className="overflow-x-auto"><table className="w-full text-sm min-w-[700px]">
|
||||||
|
<thead><tr className="text-left border-b"><th className="p-2">İhale</th><th className="p-2">Sorumlu</th><th className="p-2">Belge eksikleri</th><th className="p-2">Sıradaki kontrol</th></tr></thead>
|
||||||
|
<tbody>{tenders.map(tender=>{
|
||||||
|
const summary=preparationSummary(tender,vault,now);
|
||||||
|
return <tr key={tender.id} className="border-b align-top">
|
||||||
|
<td className="p-2"><button onClick={()=>open(tender,'source')} className="text-blue-800 underline text-left">{tender.ikn} · {tender.baslik}</button><p className="text-xs mt-1">{summary.issues.length ? `${summary.issues.length} kontrol eksik` : 'Kayıtlı kontroller tamam; son kontrol gerekli'}</p></td>
|
||||||
|
<td className="p-2">{summary.owner}</td>
|
||||||
|
<td className="p-2"><details><summary className="cursor-pointer">{summary.documentIssues.length ? `${summary.documentIssues.length} belge kontrolü` : 'Kayıtlı belgelerde eksik bulunmadı'}</summary>
|
||||||
|
<ul className="space-y-2 mt-2">{summary.documentIssues.map((text,i)=><li key={i}><button onClick={()=>open(tender,'checks')} className="text-blue-800 underline text-left">{text}</button></li>)}</ul>
|
||||||
|
</details></td>
|
||||||
|
<td className="p-2"><button onClick={()=>open(tender,summary.next.target)} className="text-blue-800 underline text-left">{summary.next.text}</button>
|
||||||
|
<details className="mt-2"><summary className="cursor-pointer text-xs">Tüm eksikleri göster</summary><ul className="space-y-2 mt-2">{summary.issues.map((item,i)=><li key={i}><button onClick={()=>open(tender,item.target)} className="text-blue-800 underline text-left">{item.text}</button></li>)}</ul></details>
|
||||||
|
</td>
|
||||||
|
</tr>;
|
||||||
|
})}</tbody>
|
||||||
|
</table></div>}
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ export const TenderDetailModal = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders'
|
const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders'
|
||||||
|
useEffect(()=>{setActiveTab(activeTenderDetail?.preparationTarget ? 'pdf_stamp' : 'details');},[activeTenderDetail?.id,activeTenderDetail?.preparationTarget]);
|
||||||
const noteOwner = authUser?.email || '';
|
const noteOwner = authUser?.email || '';
|
||||||
const [noteDrafts, setNoteDrafts] = useState(() => ({ owner: noteOwner, drafts: {} }));
|
const [noteDrafts, setNoteDrafts] = useState(() => ({ owner: noteOwner, drafts: {} }));
|
||||||
// Discard private drafts on account changes, before rendering another account.
|
// Discard private drafts on account changes, before rendering another account.
|
||||||
@@ -434,7 +435,7 @@ export const TenderDetailModal = () => {
|
|||||||
|
|
||||||
{/* Tab 2: Teklif dosyası hazırlığı */}
|
{/* Tab 2: Teklif dosyası hazırlığı */}
|
||||||
{activeTab === 'pdf_stamp' && (
|
{activeTab === 'pdf_stamp' && (
|
||||||
<BidPackageBuilder key={tender.ikn || tender.id} tender={tender}
|
<BidPackageBuilder key={tender.ikn || tender.id} tender={tender} initialFocus={activeTenderDetail?.preparationTarget}
|
||||||
results={aiAnalysisResults?.tenderKey === (tender.ikn || tender.id) ? aiAnalysisResults : null} />
|
results={aiAnalysisResults?.tenderKey === (tender.ikn || tender.id) ? aiAnalysisResults : null} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const bidFirmFields = [
|
||||||
|
{ k: 'firmaUnvan', l: 'Firma ticaret unvanı', zorunlu: true, genis: true },
|
||||||
|
{ k: 'adres', l: 'Tebligata esas adres', zorunlu: true, genis: true },
|
||||||
|
{ k: 'vergiDairesi', l: 'Vergi dairesi', zorunlu: true },
|
||||||
|
{ k: 'vergiNo', l: 'Vergi numarası', zorunlu: true },
|
||||||
|
{ k: 'sicilNo', l: 'Ticaret sicil / MERSİS no', zorunlu: false },
|
||||||
|
{ k: 'yetkiliAd', l: 'Yetkili ad soyad', zorunlu: true },
|
||||||
|
{ k: 'yetkiliGorev', l: 'Yetkilinin görevi', zorunlu: true, ph: 'Örn. Genel Müdür' },
|
||||||
|
{ k: 'telefon', l: 'Telefon', zorunlu: true },
|
||||||
|
{ k: 'eposta', l: 'E-posta', zorunlu: true },
|
||||||
|
];
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {bidFirmFields} from './bidFirmFields.js';
|
||||||
|
import {sourceRows,bidSourceOptions,bidSourceSnapshot,restoreBidPrices} from './bidSource.js';
|
||||||
|
import {selectBidLots} from './bidLots.js';
|
||||||
|
import {bidTotal,bidAmount,validateBidRows} from './bidValidation.js';
|
||||||
|
import {bidTypeForForm,bidValidityForForm} from './bidTerms.js';
|
||||||
|
import {documentKey,preparationBlockers} from './preparationChecks.js';
|
||||||
|
import {preparationDeadlineProblem} from './preparationDeadline.js';
|
||||||
|
import {tenderDateKey} from './tenderCalendar.js';
|
||||||
|
|
||||||
|
// Saved draft diagnostics only. A clean summary still requires live output checks.
|
||||||
|
export function preparationSummary(tender,vault,now=new Date()) {
|
||||||
|
const draft=tender.bidDraft || {},analysis=draft.analysisSource || {},issues=[];
|
||||||
|
const add=(target,text)=>issues.push({target,text});
|
||||||
|
const deadline=preparationDeadlineProblem(tender,now);
|
||||||
|
if(deadline)add('source',deadline);
|
||||||
|
const documents=[...new Map([...(analysis.documentChecklist || []),...(draft.manualDocuments || [])].map(d=>[documentKey(d),d])).values()];
|
||||||
|
const itemSource=draft.itemSource || {},partial=draft.partialBid || {enabled:false,source:'',selected:[],assignments:{}};
|
||||||
|
const options=bidSourceOptions(tender,analysis,itemSource),rows=sourceRows(tender,analysis,options.selected || 'tender');
|
||||||
|
const selection=selectBidLots(rows,partial);
|
||||||
|
const snapshot=JSON.stringify([bidSourceSnapshot(tender,{...analysis,documentChecklist:documents},rows),partial,itemSource,options.signature]);
|
||||||
|
if(draft.reviewedSource!==snapshot)add('source','Resmi kaynak karşılaştırması gerekli.');
|
||||||
|
if(analysis.sourceReview?.status==='partial')add('analysis','Analiz kısmi; kaynak kontrolünü tamamlayın.');
|
||||||
|
if(options.needsSelection)add('itemSource','Resmi cetvele uygun kalem listesini seçin.');
|
||||||
|
for(const field of bidFirmFields)if(field.zorunlu&&!String(draft.firma?.[field.k] || '').trim())add(`firm:${field.k}`,`${field.l} kayıtlı taslakta eksik.`);
|
||||||
|
const type=bidTypeForForm(tender.aciklama,analysis.procurementTerms?.bidType),bidType=draft.teklifTuru || type.type || '';
|
||||||
|
if(!['birim','goturu'].includes(bidType)||type.status==='conflict'||(type.type&&type.type!==bidType))add('bidType','Teklif türünü doğrulayın.');
|
||||||
|
const validity=bidValidityForForm(tender.aciklama,analysis.procurementTerms?.bidValidity),days=Number(draft.gecerlilikGun || validity.days || '');
|
||||||
|
if(!Number.isInteger(days)||days<=0||validity.status==='conflict'||(validity.days&&days<validity.days))add('validity','Teklif geçerlilik süresini doğrulayın.');
|
||||||
|
selection.errors.forEach(text=>add('lots',text));
|
||||||
|
const prices=restoreBidPrices(draft),priced=selection.rows.map(row=>({...row,birimFiyat:prices[row.priceKey]}));
|
||||||
|
if(bidType==='birim') {
|
||||||
|
if(!priced.length)add('pricing','Resmi teklif kalemleri eksik.');
|
||||||
|
priced.forEach((row,i)=>{if(validateBidRows([row]).length)add(`price:${row.priceKey}`,`${i+1}. kalemin miktar, birim veya fiyatını tamamlayın.`);});
|
||||||
|
if(priced.length&&!validateBidRows(priced).length&&bidTotal(priced)===null)add('pricing','Genel toplam geçersiz veya hesaplama sınırını aşıyor.');
|
||||||
|
}
|
||||||
|
if(bidType==='goturu'&&bidAmount(draft.goturuBedel)===null)add('lumpSum','Toplam teklif bedelini kontrol edin.');
|
||||||
|
const checkedVault={...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
|
||||||
|
const documentIssues=preparationBlockers([],documents,draft.checks,checkedVault);
|
||||||
|
preparationBlockers(selection.rows,documents,draft.checks,checkedVault).forEach(text=>add('checks',text));
|
||||||
|
return {issues,documentIssues,owner:tender.atananPersonel || 'Atanmadı',next:issues[0] || {target:'source',text:'Dosyayı açıp son kontrolleri yapın.'}};
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {preparationSummary} from '../src/services/preparationSummary.js';
|
||||||
|
import {documentKey,documentRevision} from '../src/services/preparationChecks.js';
|
||||||
|
const now=new Date('2026-09-06T10:00:00Z');
|
||||||
|
const tender={id:'test',ikn:'2026/1',tarih:'10.09.2026',saat:'10:00',baslik:'Lab',items:[{ad:'Kit',miktar:2,birim:'adet'}]};
|
||||||
|
const vault={loading:false,error:null,documents:[]};
|
||||||
|
test('five saved dossiers retain distinct owners, missing documents and actionable targets',()=>{
|
||||||
|
const rows=Array.from({length:5},(_,i)=>({...tender,id:`t-${i}`,ikn:`2026/${i}`,atananPersonel:`Sorumlu ${i}`,bidDraft:{manualDocuments:[{document:`Belge ${i}`}],teklifTuru:'birim'}}));
|
||||||
|
rows.forEach((row,i)=>{
|
||||||
|
const result=preparationSummary(row,vault,now);
|
||||||
|
assert.equal(result.owner,`Sorumlu ${i}`);
|
||||||
|
assert.ok(result.documentIssues.some(text=>text.includes(`Belge ${i}`)));
|
||||||
|
assert.equal(result.next.target,'source');
|
||||||
|
assert.ok(result.issues.some(issue=>issue.target.startsWith('price:')));
|
||||||
|
assert.ok(result.issues.some(issue=>issue.target==='firm:vergiNo'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('document revision and vault failures invalidate a previously verified saved document',()=>{
|
||||||
|
const doc={document:'Ürün belgesi'};
|
||||||
|
const file={id:'f',tur:'firma',olusturma:'2026-09-01',boyut:20,son_kullanma:'2026-12-31'};
|
||||||
|
const draft={manualDocuments:[doc],checks:{documents:{[documentKey(doc)]:{status:'verified',evidence:'Madde 2',documentId:'f',documentRevision:documentRevision(file)}}}};
|
||||||
|
assert.deepEqual(preparationSummary({...tender,bidDraft:draft},{...vault,documents:[file]},now).documentIssues,[]);
|
||||||
|
assert.ok(preparationSummary({...tender,bidDraft:draft},{...vault,documents:[{...file,boyut:21}]},now).documentIssues.some(text=>text.includes('değişti')));
|
||||||
|
assert.ok(preparationSummary({...tender,bidDraft:draft},{...vault,error:'offline'},now).documentIssues.some(text=>text.includes('doğrulanamadı')));
|
||||||
|
});
|
||||||
|
test('expired source takes priority over completion or manually moved Kanban status',()=>{
|
||||||
|
const summary=preparationSummary({...tender,tarih:'01.09.2026',kanbanStatus:'teklif_verildi'},vault,now);
|
||||||
|
assert.match(summary.next.text,/zamanı geçti/);
|
||||||
|
assert.equal(summary.next.target,'source');
|
||||||
|
assert.equal(summary.owner,'Atanmadı');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user