fix: normalize Turkish search and isolate query item matches
This commit is contained in:
@@ -235,3 +235,9 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
|
|||||||
- ISO/ofsetli zamanlar Türkiye gününe dönüştürülüyor; Türkçe kaynaklardaki ofsetsiz saatler İstanbul kabul ediliyor. Geçersiz takvim günü/saat reddediliyor; varsa ayrı ilan tarihi kullanılıyor. Süre metni veya keyfi kayıt kimliği yayın zamanı olarak yorumlanmıyor.
|
- ISO/ofsetli zamanlar Türkiye gününe dönüştürülüyor; Türkçe kaynaklardaki ofsetsiz saatler İstanbul kabul ediliyor. Geçersiz takvim günü/saat reddediliyor; varsa ayrı ilan tarihi kullanılıyor. Süre metni veya keyfi kayıt kimliği yayın zamanı olarak yorumlanmıyor.
|
||||||
- Bugün eklenenler hesabında bilinen yayın tarihi, eskimiş isYeni bayrağına üstün geliyor. Uyum puanı eşitliğinde de kimlik yerine yayın sırası kullanılıyor; sayısal id nedeniyle localeCompare hatası ortadan kalktı.
|
- Bugün eklenenler hesabında bilinen yayın tarihi, eskimiş isYeni bayrağına üstün geliyor. Uyum puanı eşitliğinde de kimlik yerine yayın sırası kullanılıyor; sayısal id nedeniyle localeCompare hatası ortadan kalktı.
|
||||||
- Kanıt: yayın zamanı regresyonuyla 63/63 test ve build geçti. UTC/Türkiye gün sınırı, ofsetsiz saat, farklı türde kimlik, geçersiz tarih ve eski isYeni bayrağı doğrulandı. Kaynakların gerçek saat dilimi sözleşmelerinin tek tek teyidi ve gerçek veriyle görsel kabul hâlâ açık.
|
- Kanıt: yayın zamanı regresyonuyla 63/63 test ve build geçti. UTC/Türkiye gün sınırı, ofsetsiz saat, farklı türde kimlik, geçersiz tarih ve eski isYeni bayrağı doğrulandı. Kaynakların gerçek saat dilimi sözleşmelerinin tek tek teyidi ve gerçek veriyle görsel kabul hâlâ açık.
|
||||||
|
|
||||||
|
### D05 — Türkçe arama ve sorguya bağlı kalem sonuçları
|
||||||
|
|
||||||
|
- Başlık/kurum/İKN/kalem araması ortak normalizasyon kullanıyor: Türkçe büyük-küçük harf, birleşik Unicode karakterleri, noktasız ı ve fazla boşluklar aynı şekilde ele alınıyor. Metin ve nesne biçimindeki kalem özetleri destekleniyor.
|
||||||
|
- Tam kalem havuzu eşleşmeleri artık üretildikleri sorgu ile saklanıyor. Kullanıcı yeni sorguya geçtiği render anında önceki sorgunun eşleşme haritası kullanılmıyor; debounce/ağ beklerken eski sonuçlar yeni aramaya karışmıyor. Mevcut geç yanıt iptal kontrolü korunuyor.
|
||||||
|
- Kanıt: Türkçe alan eşleşmeleri ve hemogram → mikroskop sorgu geçişi regresyonlarıyla 65/65 test ve build geçti. Gerçek arama sağlayıcısının tüm eşanlamlı/tıbbi ürün kapsamı ve tarayıcıda geciktirilmiş ağ kabulü henüz doğrulanmadı.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesTenderSearch } from '../services/tenderSearch';
|
||||||
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { TenderCard } from './TenderCard';
|
import { TenderCard } from './TenderCard';
|
||||||
@@ -43,17 +44,7 @@ export const TenderList = () => {
|
|||||||
const filteredTenders = useMemo(() => {
|
const filteredTenders = useMemo(() => {
|
||||||
return tenders.filter(tender => {
|
return tenders.filter(tender => {
|
||||||
// Search Query
|
// Search Query
|
||||||
if (searchQuery) {
|
if (!matchesTenderSearch(tender,searchQuery,kalemEslesen)) return false;
|
||||||
const q = searchQuery.toLowerCase();
|
|
||||||
const matchTitle = (tender.baslik || '').toLowerCase().includes(q);
|
|
||||||
const matchKurum = (tender.kurum || '').toLowerCase().includes(q);
|
|
||||||
const matchIkn = (tender.ikn || '').toLowerCase().includes(q);
|
|
||||||
const matchItems = tender.items && tender.items.some(i => (i.ad || '').toLowerCase().includes(q));
|
|
||||||
// Kartta taşınan ilk kalemler + sunucudaki tam kalem araması
|
|
||||||
const matchKalem = tender.kalemler && tender.kalemler.some(a => (a || '').toLowerCase().includes(q));
|
|
||||||
const matchHavuz = kalemEslesen && tender.ikn && kalemEslesen.has(tender.ikn);
|
|
||||||
if (!matchTitle && !matchKurum && !matchIkn && !matchItems && !matchKalem && !matchHavuz) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Geçmiş ihaleler: varsayılan olarak GİZLİ. Kullanıcı "geçmişi de göster"
|
// Geçmiş ihaleler: varsayılan olarak GİZLİ. Kullanıcı "geçmişi de göster"
|
||||||
// derse (onlyValidOnly=false) hepsi gelir. Tarihi okunamayan kayıt gizlenmez.
|
// derse (onlyValidOnly=false) hepsi gelir. Tarihi okunamayan kayıt gizlenmez.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch';
|
||||||
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
|
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
|
import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
|
||||||
import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum';
|
import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum';
|
||||||
@@ -54,19 +55,20 @@ export const AppProvider = ({ children }) => {
|
|||||||
// Kalem havuzunda arama: firmanın kendi ürününü ("eldiven", "ortopedik vida")
|
// Kalem havuzunda arama: firmanın kendi ürününü ("eldiven", "ortopedik vida")
|
||||||
// hangi ihalelerin teklif cetvelinde geçtiğini bulması için. Kartta yalnızca
|
// hangi ihalelerin teklif cetvelinde geçtiğini bulması için. Kartta yalnızca
|
||||||
// ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor.
|
// ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor.
|
||||||
const [kalemEslesen, setKalemEslesen] = useState(null); // Map(ikn → eşleşen kalemler)
|
const [kalemSonucu, setKalemSonucu] = useState(null);
|
||||||
|
const kalemEslesen = matchingItemResults(kalemSonucu, searchQuery);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const q = searchQuery.trim();
|
const q = searchQuery.trim();
|
||||||
if (q.length < 2) { setKalemEslesen(null); return; }
|
if (q.length < 2) { setKalemSonucu(null); return; }
|
||||||
let iptal = false;
|
let iptal = false;
|
||||||
const zaman = setTimeout(() => {
|
const zaman = setTimeout(() => {
|
||||||
import('../services/kalem')
|
import('../services/kalem')
|
||||||
.then(({ kalemAra }) => kalemAra(q))
|
.then(({ kalemAra }) => kalemAra(q))
|
||||||
.then((sonuclar) => {
|
.then((sonuclar) => {
|
||||||
if (iptal) return;
|
if (iptal) return;
|
||||||
setKalemEslesen(sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null);
|
setKalemSonucu({query:normalizeTenderSearch(q),matches:sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null});
|
||||||
})
|
})
|
||||||
.catch(() => { if (!iptal) setKalemEslesen(null); });
|
.catch(() => { if (!iptal) setKalemSonucu(null); });
|
||||||
}, 350);
|
}, 350);
|
||||||
return () => { iptal = true; clearTimeout(zaman); };
|
return () => { iptal = true; clearTimeout(zaman); };
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export const normalizeTenderSearch = value => String(value ?? '').normalize('NFKD')
|
||||||
|
.toLocaleLowerCase('tr-TR').replace(/ı/g,'i').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim();
|
||||||
|
|
||||||
|
export function matchingItemResults(result, query) {
|
||||||
|
return result?.query === normalizeTenderSearch(query) ? result.matches : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesTenderSearch(tender, query, itemMatches) {
|
||||||
|
const q=normalizeTenderSearch(query);
|
||||||
|
if (!q) return true;
|
||||||
|
const values=[tender.baslik,tender.kurum,tender.ikn,
|
||||||
|
...(tender.items || []).map(item=>item.ad),
|
||||||
|
...(tender.kalemler || []).map(item=>typeof item === 'string' ? item : item.ad)];
|
||||||
|
return values.some(v=>normalizeTenderSearch(v).includes(q)) || Boolean(itemMatches?.has(tender.ikn));
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {normalizeTenderSearch,matchesTenderSearch,matchingItemResults} from '../src/services/tenderSearch.js';
|
||||||
|
test('Turkish search treats case, decomposed characters and spacing consistently across fields',()=>{
|
||||||
|
for(const value of ['IĞDIR','Iğdır','igdir',' IĞDIR ']) assert.equal(normalizeTenderSearch(value),'igdir');
|
||||||
|
for(const field of ['baslik','kurum']) assert.equal(matchesTenderSearch({[field]:'IĞDIR İL SAĞLIK'},'ığdır'),true);
|
||||||
|
assert.equal(matchesTenderSearch({items:[{ad:'STERİL TEST KİTİ'}]},'steril test'),true);
|
||||||
|
assert.equal(matchesTenderSearch({kalemler:['İmmünoloji']},'immunoloji'),true);
|
||||||
|
assert.equal(matchesTenderSearch({kalemler:[{ad:'Ölçüm'}]},'olcum'),true);
|
||||||
|
assert.equal(matchesTenderSearch({},' '),true);
|
||||||
|
assert.equal(matchesTenderSearch({baslik:'Mikroskop'},'hemogram'),false);
|
||||||
|
});
|
||||||
|
test('old item matches are unavailable immediately when a new query is entered',()=>{
|
||||||
|
const result={query:normalizeTenderSearch('hemogram'),matches:new Map([['2026/1',[{ad:'Hemogram'}]]])};
|
||||||
|
assert.equal(matchingItemResults(result,'HEMOGRAM'),result.matches);
|
||||||
|
assert.equal(matchingItemResults(result,'mikroskop'),null);
|
||||||
|
assert.equal(matchesTenderSearch({ikn:'2026/1',baslik:'Genel sarf'},'mikroskop',matchingItemResults(result,'mikroskop')),false);
|
||||||
|
assert.equal(matchingItemResults(result,''),null);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user