Distinguish shared item inventory from tender rows and load errors
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"scenario": "One tender-record item, empty shared document inventory",
|
||||||
|
"messageConsistent": true,
|
||||||
|
"text": "Listeye Geri Dön\nİncelendi olarak işaretle\nTakip Et\n🗂 Hazırlık Görevi Aç\nMal Alımı\nİKN: 2026/1582991\nEkleme:\nQA — Giresun laboratuvar\nKurum Adı\nGiresun TEST KURUMU\nİhale Tarihi & Saat\n10.09.2026 - 10:00\nŞehir / Bölge\nGiresun ()\nYaklaşık Maliyet\nGizli / Belirtilmemiş\nİhale Detayları\nBirim Fiyat Cetveli (PDF & Kaşe)\nİnteraktif\nYapay Zeka Asistanı\nHazırlık Checklist\nTeklif Asistanı\nAI\nİhale Açıklaması & Teslimat Koşulları\n\nBu yalnızca yerel arayüz test verisidir. Teklif geçerlilik süresi 120 takvim günü. Birim fiyat teklif. Bu yalnızca yerel arayüz test verisidir. Teklif geçerlilik süresi 120 takvim günü. Birim fiyat teklif. Bu yalnızca yerel arayüz test verisidir. Teklif geçerlilik süresi 120 takvim günü. Birim fiyat teklif.\n\nTeslimat Süresi\nTeslimat Yeri\nTeklif Cetveli Kalemleri\n\nİhale kaydında aşağıda gösterilen 1 kalem var. Ortak doküman havuzunda ayrıca çıkarılmış bir cetvel bulunamadı; mevcut kalemleri resmi dokümanla karşılaştırın. İhale dokümanından ortak havuza aktarılan kalemler diğer kullanıcılara da görünür.\n\nMalzeme / Kalem Listesi (1 Kalem)\n#\tMalzeme Adı\tMiktar\tTahmini Birim Fiyat\tToplam Tutarlar\n1\tSentetik test kalemi\t2 adet\t—\t—\nEkip Notları\nNot Ekle\nKUR FARKI HESAPLAYICI\nTCMB döviz satış\n\nTCMB bülteni: 04.09.2026 · Alındı: 06.09.2026 10:13:15\n\nUSD / TRY\n₺48.3195\nEUR / TRY\n₺56.1581\nTeklif Tutarı (TL)\nDolar Karşılığı:\n$310.43\nEuro Karşılığı:\n€267.10\nİHALE DOSYALARI & TAKVİM\nBirim Fiyat Cetveli (PDF Aç/Düzenle)\nGoogle / Outlook Takvime Ekle"
|
||||||
|
}
|
||||||
@@ -481,3 +481,13 @@ Mevcut localhost:5175 QA sunucusunun canlı olduğu doğrulandı; yeniden başla
|
|||||||
Değişiklikten sonra aynı tarayıcı akışı tekrarlandı: yeni alert, aynı taslak ve kullanılabilir Denetle düğmesi doğrulandı. Kanıtlar: `qa-offer-provider-error.json`, `qa-offer-provider-error-after.json`. Bu kayıtlar sağlayıcı yokluğu hata yolunu kanıtlar; gerçek AI üretimi, iki hesaplı tarayıcı geçişi, uzun analiz veya başarılı çıktı kabulü değildir. Yeni backend metni çalışan eski sunucuda ayrıca sınanmadı; tarayıcı yeni istemci dönüşümünü doğruladı.
|
Değişiklikten sonra aynı tarayıcı akışı tekrarlandı: yeni alert, aynı taslak ve kullanılabilir Denetle düğmesi doğrulandı. Kanıtlar: `qa-offer-provider-error.json`, `qa-offer-provider-error-after.json`. Bu kayıtlar sağlayıcı yokluğu hata yolunu kanıtlar; gerçek AI üretimi, iki hesaplı tarayıcı geçişi, uzun analiz veya başarılı çıktı kabulü değildir. Yeni backend metni çalışan eski sunucuda ayrıca sınanmadı; tarayıcı yeni istemci dönüşümünü doğruladı.
|
||||||
|
|
||||||
Tüm 104 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın kalan kapsamı açık; canlı yayın yapılmadı.
|
Tüm 104 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın kalan kapsamı açık; canlı yayın yapılmadı.
|
||||||
|
|
||||||
|
### D05 / D09 — Kalem kaynakları ve boş/hata durumu ayrımı
|
||||||
|
|
||||||
|
Tarayıcıda ihale kaydının bir kalemi görüntülenirken ortak havuz panelinde “kalem listesi henüz yok” mesajı görülmüştü. Panel artık ihale kaydında kalem varsa sayısını açıkça belirtir ve yalnız ortak doküman havuzunda ayrı cetvel bulunamadığını söyler. Kaynaklar sessizce birleştirilmedi; resmi dokümanla kontrol gereği korunur. Ortak liste doluyken başlık kaynağını belirtir.
|
||||||
|
|
||||||
|
Ortak havuz istemcisi HTTP hata/bozuk yanıtı boş liste gibi sunmaz. Panel hata ve yeniden yükleme düğmesi gösterir. Otuz saniyelik bekleme sınırı, ihale değişiminde iptal ve İKN başına yeniden oluşturulan panel durumu eklendi; önceki ihalenin filtre/kalemleri yeni ihaleye taşınmaz.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -9,19 +9,23 @@ import { kalemleriGetir } from '../services/kalem';
|
|||||||
* sunucu otomatik indiremiyor. Bir kullanıcı ihale dokümanını yüklediğinde
|
* sunucu otomatik indiremiyor. Bir kullanıcı ihale dokümanını yüklediğinde
|
||||||
* kalemler ayrıştırılıp havuza yazılıyor ve bu liste herkese görünür oluyor.
|
* kalemler ayrıştırılıp havuza yazılıyor ve bu liste herkese görünür oluyor.
|
||||||
*/
|
*/
|
||||||
export const TeklifCetveliKalemleri = ({ ikn }) => {
|
export const TeklifCetveliKalemleri = ({ikn, knownItemCount = 0}) => <SharedItems key={ikn || 'unknown'} ikn={ikn} knownItemCount={knownItemCount} />;
|
||||||
|
|
||||||
|
const SharedItems = ({ikn, knownItemCount}) => {
|
||||||
const [durum, setDurum] = useState({ yukleniyor: true, kalemler: [], kaynak: null });
|
const [durum, setDurum] = useState({ yukleniyor: true, kalemler: [], kaynak: null });
|
||||||
const [suzgec, setSuzgec] = useState('');
|
const [suzgec, setSuzgec] = useState('');
|
||||||
|
const [retry, setRetry] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ikn) { setDurum({ yukleniyor: false, kalemler: [], kaynak: null }); return; }
|
if (!ikn) { setDurum({ yukleniyor: false, kalemler: [], kaynak: null }); return; }
|
||||||
let iptal = false;
|
let iptal = false;
|
||||||
setDurum((d) => ({ ...d, yukleniyor: true }));
|
const controller = new AbortController();
|
||||||
kalemleriGetir(ikn)
|
setDurum({yukleniyor:true,kalemler:[],kaynak:null});
|
||||||
|
kalemleriGetir(ikn, {signal:controller.signal})
|
||||||
.then((d) => { if (!iptal) setDurum({ yukleniyor: false, kalemler: d.kalemler || [], kaynak: d.kaynak }); })
|
.then((d) => { if (!iptal) setDurum({ yukleniyor: false, kalemler: d.kalemler || [], kaynak: d.kaynak }); })
|
||||||
.catch(() => { if (!iptal) setDurum({ yukleniyor: false, kalemler: [], kaynak: null }); });
|
.catch(error => { if (!iptal) setDurum({ yukleniyor:false, kalemler:[], kaynak:null, hata:error.message }); });
|
||||||
return () => { iptal = true; };
|
return () => { iptal = true; controller.abort(); };
|
||||||
}, [ikn]);
|
}, [ikn, retry]);
|
||||||
|
|
||||||
if (durum.yukleniyor) {
|
if (durum.yukleniyor) {
|
||||||
return (
|
return (
|
||||||
@@ -31,6 +35,11 @@ export const TeklifCetveliKalemleri = ({ ikn }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (durum.hata) return <div role="alert" className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs text-amber-900">
|
||||||
|
<p>Ortak doküman kalemleri yüklenemedi. {durum.hata}</p>
|
||||||
|
<button type="button" onClick={() => setRetry(n => n + 1)} className="mt-2 underline font-semibold">Kalemleri yeniden yükle</button>
|
||||||
|
</div>;
|
||||||
|
|
||||||
if (!durum.kalemler.length) {
|
if (!durum.kalemler.length) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||||
@@ -38,9 +47,11 @@ export const TeklifCetveliKalemleri = ({ ikn }) => {
|
|||||||
<Package className="w-4 h-4 text-indigo-600" /> Teklif Cetveli Kalemleri
|
<Package className="w-4 h-4 text-indigo-600" /> Teklif Cetveli Kalemleri
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-600 leading-relaxed mt-1.5">
|
<p className="text-xs text-slate-600 leading-relaxed mt-1.5">
|
||||||
Bu ihalenin kalem listesi henüz havuzda yok. Kalemler EKAP'ın API'sinde yayımlanmıyor;
|
{knownItemCount > 0
|
||||||
yalnızca ihale dokümanının içinde bulunuyor. Aşağıdan dokümanı yüklerseniz kalemler
|
? `İhale kaydında aşağıda gösterilen ${knownItemCount} kalem var. Ortak doküman havuzunda ayrıca çıkarılmış bir cetvel bulunamadı; mevcut kalemleri resmi dokümanla karşılaştırın.`
|
||||||
otomatik çıkarılır ve bu ihale <strong>tüm kullanıcılarda</strong> kalemleriyle listelenir.
|
: 'Ortak doküman havuzunda bu ihale için kalem bulunamadı. Resmi ihale dokümanını yükleyerek kalemleri çıkarmayı deneyebilirsiniz.'}
|
||||||
|
{' '}İhale dokümanından ortak havuza aktarılan kalemler diğer kullanıcılara da görünür.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -56,7 +67,7 @@ export const TeklifCetveliKalemleri = ({ ikn }) => {
|
|||||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||||
<h3 className="font-bold text-sm text-slate-900 flex items-center gap-2">
|
<h3 className="font-bold text-sm text-slate-900 flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-indigo-600" />
|
<Package className="w-4 h-4 text-indigo-600" />
|
||||||
Teklif Cetveli Kalemleri ({durum.kalemler.length})
|
Ortak Doküman Kalemleri ({durum.kalemler.length})
|
||||||
</h3>
|
</h3>
|
||||||
<label className="relative">
|
<label className="relative">
|
||||||
<Search className="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2" />
|
<Search className="w-3.5 h-3.5 text-slate-400 absolute left-2.5 top-1/2 -translate-y-1/2" />
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ export const TenderDetailModal = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Ortak havuzdan teklif cetveli kalemleri */}
|
{/* Ortak havuzdan teklif cetveli kalemleri */}
|
||||||
<TeklifCetveliKalemleri ikn={tender.ikn} />
|
<TeklifCetveliKalemleri ikn={tender.ikn} knownItemCount={tender.items?.length || 0} />
|
||||||
|
|
||||||
{/* Material Table */}
|
{/* Material Table */}
|
||||||
{tender.items?.length > 0 && (
|
{tender.items?.length > 0 && (
|
||||||
|
|||||||
+15
-5
@@ -8,11 +8,21 @@
|
|||||||
import { getToken } from './auth.js';
|
import { getToken } from './auth.js';
|
||||||
|
|
||||||
/** Bir ihalenin tüm kalemleri. */
|
/** Bir ihalenin tüm kalemleri. */
|
||||||
export async function kalemleriGetir(ikn) {
|
export async function kalemleriGetir(ikn, {signal, timeoutMs = 30000} = {}) {
|
||||||
const r = await fetch(`/api/kalem?ikn=${encodeURIComponent(ikn)}`);
|
const controller = new AbortController();
|
||||||
const d = await r.json().catch(() => ({}));
|
const cancel = () => controller.abort();
|
||||||
if (!d.success) throw new Error(d.error || 'kalemler alınamadı');
|
if (signal?.aborted) cancel(); else signal?.addEventListener('abort',cancel,{once:true});
|
||||||
return d;
|
let timedOut = false;
|
||||||
|
const timer = setTimeout(() => {timedOut=true;controller.abort();},timeoutMs);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/kalem?ikn=${encodeURIComponent(ikn)}`,{signal:controller.signal});
|
||||||
|
const d = await r.json().catch(() => null);
|
||||||
|
if (!r.ok || d?.success !== true || !Array.isArray(d.kalemler)) throw new Error('Kalem listesi alınamadı. Yeniden deneyin.');
|
||||||
|
return d;
|
||||||
|
} catch (error) {
|
||||||
|
if (timedOut) throw new Error('Kalem listesi yüklenirken zaman aşımı oluştu.');
|
||||||
|
throw error;
|
||||||
|
} finally {clearTimeout(timer);signal?.removeEventListener('abort',cancel);}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kalem metninde arama — eşleşen İKN'ler. */
|
/** Kalem metninde arama — eşleşen İKN'ler. */
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {test} from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {kalemleriGetir} from '../src/services/kalem.js';
|
||||||
|
test('empty shared inventory is distinct from HTTP errors and malformed responses',async()=>{
|
||||||
|
const original=globalThis.fetch;
|
||||||
|
try {
|
||||||
|
globalThis.fetch=async()=>Response.json({success:true,kalemler:[]});
|
||||||
|
assert.deepEqual((await kalemleriGetir('2026/123')).kalemler,[]);
|
||||||
|
for(const response of [Response.json({success:true,kalemler:[]},{status:503}),Response.json({success:true}),new Response('Bad gateway')]) {
|
||||||
|
globalThis.fetch=async()=>response;await assert.rejects(kalemleriGetir('2026/123'),/alınamadı/);
|
||||||
|
}
|
||||||
|
} finally {globalThis.fetch=original;}
|
||||||
|
});
|
||||||
|
test('shared inventory waits are bounded and can be cancelled on tender change',async()=>{
|
||||||
|
const original=globalThis.fetch;
|
||||||
|
try {
|
||||||
|
globalThis.fetch=async(url,{signal})=>new Promise((resolve,reject)=>{
|
||||||
|
const cancel=()=>reject(new DOMException('Aborted','AbortError'));
|
||||||
|
if(signal.aborted)cancel();else signal.addEventListener('abort',cancel,{once:true});
|
||||||
|
});
|
||||||
|
await assert.rejects(kalemleriGetir('2026/123',{timeoutMs:5}),/zaman aşımı/);
|
||||||
|
const c=new AbortController();const pending=kalemleriGetir('2026/123',{signal:c.signal});c.abort();
|
||||||
|
await assert.rejects(pending,{name:'AbortError'});
|
||||||
|
} finally {globalThis.fetch=original;}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user