From 3a5b06cee2c136a498428e48d33eeeb73119f1ae Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 15:59:01 +0300 Subject: [PATCH] fix: combine detailed tender search fields --- docs/quality/uygulama-durumu.md | 8 +++ src/components/DetailedSearchModal.jsx | 70 +++++++++++++++----------- src/components/Header.jsx | 1 + src/components/LeftSidebar.jsx | 4 +- src/components/TenderList.jsx | 12 +++-- src/context/AppContext.jsx | 4 ++ src/services/tenderSearch.js | 5 ++ tests/tender-search.test.mjs | 9 +++- 8 files changed, 78 insertions(+), 35 deletions(-) diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index c4cd465..5495147 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -1232,3 +1232,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Ürün araması native dialog üst katmanında, kılavuz ise özel portal penceresindeydi. ? ile kılavuz açılınca ortak izolasyon ürün aramasını inert yapıyor fakat native üst katman nedeniyle odak kılavuza geçemiyordu. Bu çakışma tarayıcıda tekrar üretildi. Ürün araması diğer pencerelerle aynı modal yığınını kullanıyor; gövde kaydırma kilidi ve arama durumunun korunması devam ediyor. - On iki tarayıcı kabulü geçti: detaylı arama ilk odağı/ileri-geri Tab, görünüm sınırları, beş alan etiketi, Kayseri+laboratuvar tek sonucu; ürün araması odağı/Tab; ürün üstünde kılavuz ve geri dönüş; sentetik ürün sonucundan doğru ihale detayına geçiş; detay kapanınca başlıktaki ÜRÜN ARA düğmesine dönüş; yeniden açıldığında önceki sorgu ve sonucun korunması. Kanıt `rapor/qa-search-modals-focus.json`. Kılavuz kapanış ölçümü QA düzeneği sıcak yenilenirken alındığı için temiz oturumda tekrarlandı; son ölçüm geçti. - QA sunucusu yalnız “QA pencere testi” sorgusuna bir sentetik ürün sonucu döndürüyor; bu kaynak arama kalitesinin testi değildir. 275 test ve build başarılı. Native dialog yerine ortak yığın kullanımı nedeniyle mobil tarayıcı/gerçek ekran okuyucu kabulü ayrıca gereklidir. Genel plan açık. + +### 2026-09-06 — Detaylı arama alanlarını bağımsız ve birlikte uygulama + +- Detaylı arama kurum adını veya İKN'yi ana anahtar kelime alanına kopyalıyordu. Kurum doluysa İKN ve anahtar kelime, yalnız İKN doluysa anahtar kelime sessizce yok sayılıyordu. Anahtar kelime, kurum adı ve kayıt numarası artık ayrı durumlarda tutuluyor ve AND koşuluyla birlikte uygulanıyor. Türkçe karakter normalizasyonu üç aramada da korunuyor. +- Pencere içindeki anahtar kelime, şehir, alım tipi, sektörler, kurum tipi, açık ihale seçimi, kurum adı ve İKN taslak olarak düzenleniyor. Kullanıcı İhaleleri Ara demeden kapatırsa etkin liste değişmiyor; yeniden açıldığında sunucuda uygulanmış filtreler yükleniyor. Temizle önce yalnız formu temizliyor, İhaleleri Ara ile bütün form ölçütleri listeye uygulanıyor. +- Kurum adı ve İKN etkin olduğunda liste üstünde ayrı görünür ölçütler ve ayrı kaldırma düğmeleri var. Ana başlıktaki anahtar kelime temizleme düğmesine erişilebilir ad eklendi. Sol menüdeki Filtreleri Sıfırla düğmesi ayrıntılı kurum/İKN alanlarını da kapsıyor. Kaydedilmiş filtre uygulanınca eski kurum/İKN ölçütleri temizleniyor. +- On beş tarayıcı kontrolü geçti: uygulanmamış taslağın listeyi değiştirmemesi; üç alanın tek Kayseri sonucu vermesi; çelişen kurum + doğru İKN'nin sıfır sonuç vermesi; ölçütlerin bağımsız kaldırılması; sol menü sıfırlaması; kurum tipi alanı; altı ölçütün birlikte uygulanması ve yerel Temizle akışı. Kanıt `rapor/qa-detailed-search-fields.json`. Tarayıcı konsolunda hata yoktu. +- `matchesDetailedSearch` için kurum ve İKN'nin birlikte/bağımsız eşleşmesini doğrulayan regresyon testi eklendi. 276 test ve build başarılı. Test verisi beş sentetik ihaledir; resmi kaynak alanlarının doğruluğunu kanıtlamaz. Genel plan açık. diff --git a/src/components/DetailedSearchModal.jsx b/src/components/DetailedSearchModal.jsx index 9210df7..bb37049 100644 --- a/src/components/DetailedSearchModal.jsx +++ b/src/components/DetailedSearchModal.jsx @@ -5,37 +5,34 @@ import { CITIES_LIST, INSTITUTION_TYPES_LIST } from '../data/constants'; import { SlidersHorizontal, Search, RotateCcw, X } from 'lucide-react'; export const DetailedSearchModal = () => { - const { - isDetailedSearchOpen, - setIsDetailedSearchOpen, - searchQuery, - setSearchQuery, - selectedCity, - setSelectedCity, - selectedAlimTuru, - setSelectedAlimTuru, - selectedSektorler, - setSelectedSektorler, - toggleSektor, - sektorListesi, - selectedKurumTipi, - setSelectedKurumTipi, - onlyValidOnly, - setOnlyValidOnly, - setActiveTab - } = useApp(); + const {isDetailedSearchOpen}=useApp(); + return isDetailedSearchOpen?:null; +}; + +const DetailedSearchForm = () => { + const context=useApp(); + const {setIsDetailedSearchOpen,setActiveTab,sektorListesi}=context; const titleId=useId(); - const modalFocus=useModalFocus(isDetailedSearchOpen,()=>setIsDetailedSearchOpen(false)); - - const [iknQuery, setIknQuery] = useState(''); - const [kurumQuery, setKurumQuery] = useState(''); - - if (!isDetailedSearchOpen) return null; + const modalFocus=useModalFocus(true,()=>setIsDetailedSearchOpen(false)); + const [searchQuery,setSearchQuery]=useState(context.searchQuery); + const [selectedCity,setSelectedCity]=useState(context.selectedCity); + const [selectedAlimTuru,setSelectedAlimTuru]=useState(context.selectedAlimTuru); + const [selectedKurumTipi,setSelectedKurumTipi]=useState(context.selectedKurumTipi); + const [selectedSektorler,setSelectedSektorler]=useState(context.selectedSektorler); + const [onlyValidOnly,setOnlyValidOnly]=useState(context.onlyValidOnly); + const [iknQuery,setIknQuery]=useState(context.detailedQueries.registration); + const [kurumQuery,setKurumQuery]=useState(context.detailedQueries.institution); + const toggleSektor=sector=>setSelectedSektorler(old=>old.includes(sector)?old.filter(value=>value!==sector):[...old,sector]); const handleApplySearch = (e) => { e.preventDefault(); - if (kurumQuery) setSearchQuery(kurumQuery); - else if (iknQuery) setSearchQuery(iknQuery); + context.setSearchQuery(searchQuery.trim()); + context.setDetailedQueries({institution:kurumQuery.trim(),registration:iknQuery.trim()}); + context.setSelectedCity(selectedCity); + context.setSelectedAlimTuru(selectedAlimTuru); + context.setSelectedKurumTipi(selectedKurumTipi); + context.setSelectedSektorler(selectedSektorler); + context.setOnlyValidOnly(onlyValidOnly); setActiveTab('list'); setIsDetailedSearchOpen(false); @@ -45,8 +42,9 @@ export const DetailedSearchModal = () => { setSearchQuery(''); setSelectedCity(''); setSelectedAlimTuru(''); - setSelectedSektorler([]); setSelectedKurumTipi(''); + setSelectedSektorler([]); + setOnlyValidOnly(true); setIknQuery(''); setKurumQuery(''); }; @@ -72,6 +70,7 @@ export const DetailedSearchModal = () => { {/* Form Body */}
+

Doldurduğunuz alanlar birlikte uygulanır. Değişiklikler İhaleleri Ara düğmesine bastığınızda listeye yansır.

@@ -82,7 +81,7 @@ export const DetailedSearchModal = () => { type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - placeholder="Başlık, açıklama içinde aranır..." + placeholder="Başlık, kurum, İKN veya kalemde aranır..." className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800 focus:ring-2 focus:ring-blue-500" />
@@ -168,6 +167,19 @@ export const DetailedSearchModal = () => { +
+ + +
+
diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 913cbd1..b7c5afe 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -67,6 +67,7 @@ export const Header = () => { {searchQuery && ( +
)} {selectedRegion&&
İl bazında bölge filtresi: {selectedRegion}
} {/* Header bar above list */} diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index 3421cba..c573e04 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -56,6 +56,7 @@ export const AppProvider = ({ children }) => { const [liveStats, setLiveStats] = useState({ toplamIhale: 0, bugunEklenen: 0, arsiv: 0 }); const [searchQuery, setSearchQuery] = useState(''); + const [detailedQueries,setDetailedQueries]=useState({institution:'',registration:''}); // 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 @@ -734,6 +735,7 @@ export const AppProvider = ({ children }) => { const applySavedFilter = filter => { try { const criteria=savedFilterCriteria(filter); + setDetailedQueries({institution:'',registration:''}); setSearchQuery('');setSelectedCpvler([]);setQuickFilter('all'); setSelectedCity(criteria.city);setSelectedRegion(criteria.region); setSelectedAlimTuru(criteria.procedure);setSelectedKurumTipi(criteria.institution); @@ -743,6 +745,7 @@ export const AppProvider = ({ children }) => { }; const clearAllFilters = () => { + setDetailedQueries({institution:'',registration:''}); setSelectedRegion(''); setSearchQuery(''); setSelectedCity(''); @@ -775,6 +778,7 @@ export const AppProvider = ({ children }) => { handleLogout, liveStats, searchQuery, + detailedQueries,setDetailedQueries, kalemEslesen, itemSearchState, retryItemSearch, setSearchQuery, selectedCity, selectedRegion,setSelectedRegion,applySavedFilter, diff --git a/src/services/tenderSearch.js b/src/services/tenderSearch.js index da8021b..1c036df 100644 --- a/src/services/tenderSearch.js +++ b/src/services/tenderSearch.js @@ -5,6 +5,11 @@ export function matchingItemResults(result, query) { return result?.query === normalizeTenderSearch(query) ? result.matches : null; } +export function matchesDetailedSearch(tender, {institution='',registration=''} = {}) { + return normalizeTenderSearch(tender.kurum).includes(normalizeTenderSearch(institution)) && + normalizeTenderSearch(tender.ikn).includes(normalizeTenderSearch(registration)); +} + export function matchesTenderSearch(tender, query, itemMatches) { const q=normalizeTenderSearch(query); if (!q) return true; diff --git a/tests/tender-search.test.mjs b/tests/tender-search.test.mjs index 6d55355..8460e64 100644 --- a/tests/tender-search.test.mjs +++ b/tests/tender-search.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import {normalizeTenderSearch,matchesTenderSearch,matchingItemResults} from '../src/services/tenderSearch.js'; +import {normalizeTenderSearch,matchesTenderSearch,matchingItemResults,matchesDetailedSearch} 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); @@ -10,6 +10,13 @@ test('Turkish search treats case, decomposed characters and spacing consistently assert.equal(matchesTenderSearch({},' '),true); assert.equal(matchesTenderSearch({baslik:'Mikroskop'},'hemogram'),false); }); +test('detailed institution and registration fields are independent and combine with AND',()=>{ + const tender={kurum:'IĞDIR İL SAĞLIK MÜDÜRLÜĞÜ',ikn:'2026/1582991'}; + assert.equal(matchesDetailedSearch(tender,{institution:'ığdır',registration:'1582991'}),true); + assert.equal(matchesDetailedSearch(tender,{institution:'ığdır',registration:'1534665'}),false); + assert.equal(matchesDetailedSearch(tender,{institution:'kayseri',registration:'1582991'}),false); + assert.equal(matchesDetailedSearch(tender,{institution:'',registration:''}),true); +}); 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);