feat: preserve search text in saved tender filters

This commit is contained in:
2026-09-06 16:03:49 +03:00
parent e4a1fd0f5b
commit bcfeb6995c
5 changed files with 35 additions and 4 deletions
+6
View File
@@ -1246,3 +1246,9 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Kod incelemesinde, güncel portföy ürününü alan ve kontrolleri sıfırlayan düğmenin yalnız setChecks çağırdığı görüldü. Üst kapsayıcıdaki onChange düğme tıklamasında çalışmadığından kaydedilmiş taslak etiketi değişmeden kalabiliyordu.
- PreparationChecks üzerinden gelen bütün kontrol güncellemeleri artık taslağı kaydedilmemiş olarak işaretliyor. Fonksiyon biçimindeki state güncellemeleri korunuyor; başarılı kayıt mevcut mekanizmayla uyarıyı kaldırıyor.
- Build ve git diff --check başarılı. Bu küçük durum bildirimi düzeltmesi için tarayıcı kabulü ve tam test paketi yeniden çalıştırılmadı. Build büyük paket uyarısı devam ediyor. Genel plan açık.
### 2026-09-06 — Kayıtlı filtrelerde arama metinleri
- Kayıtlı filtre oluşturma ekranına anahtar kelime, kurum adı ve İKN / DT no alanları eklendi. Bu alanlar mevcut revizyon kontrollü saved_filters kaydında saklanıyor. Filtre uygulanırken ana arama ve detaylı arama durumlarına ayrı aktarılıyor; dolu alanlar mevcut liste eşleştirmesinde birlikte uygulanıyor.
- Önceki filtre kayıtları yeni alanlar olmadan çalışmaya devam ediyor ve önceki arama metinlerini temizliyor. Yeni metin alanları okunurken tür doğrulaması ve kenar boşluk temizliği uygulanıyor.
- JSON çevriminden sonra üç bağımsız alanın korunması ve yanlış türdeki alanların reddi için regresyon testi eklendi. 277 test, build ve diff kontrolü başarılı. Yeni formun tarayıcıda kaydet/yenile/uygula kabulü henüz yapılmadı; etkin detaylı aramayı tek düğmeyle kayıt formuna aktarma eklenmedi. Genel plan açık.
+18
View File
@@ -7,6 +7,9 @@ export const SmartFilterBuilder = () => {
const { savedFilters, applySavedFilter, addSavedFilter, deleteSavedFilter, setActiveTab, sektorListesi, savingCount, saveError } = useApp();
const [filterName, setFilterName] = useState('');
const [keyword,setKeyword] = useState('');
const [institutionName,setInstitutionName] = useState('');
const [registration,setRegistration] = useState('');
const [selectedKurumTip, setSelectedKurumTip] = useState('');
const [selectedUsul, setSelectedUsul] = useState('');
const [selectedCity, setSelectedCity] = useState('');
@@ -33,6 +36,9 @@ export const SmartFilterBuilder = () => {
setSuccessMessage('');
const saved=await addSavedFilter({
name: filterName.trim(),
anahtarKelime: keyword.trim(),
kurumAdi: institutionName.trim(),
ikn: registration.trim(),
kurumTipi: selectedKurumTip,
usul: selectedUsul,
sehir: selectedCity,
@@ -139,6 +145,18 @@ export const SmartFilterBuilder = () => {
<p className="text-[11px] text-slate-400">Bu ad kayıtlı filtre listenizde görünür.</p>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-5 space-y-3">
<h3 className="font-bold text-sm">Arama metinleri</h3>
<p className="text-xs text-slate-600">Dolu alanlar birlikte uygulanır. Boş bıraktığınız alan aramayı sınırlamaz.</p>
{[
['Anahtar kelime',keyword,setKeyword],
['Kurum adı',institutionName,setInstitutionName],
['İKN / DT no',registration,setRegistration],
].map(([label,value,setValue])=><label key={label} className="block text-xs font-semibold text-slate-600">
{label}<input type="text" value={value} onChange={e=>setValue(e.target.value)} className="mt-1 w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-sm"/>
</label>)}
</div>
{/* Middle Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+2 -2
View File
@@ -735,8 +735,8 @@ export const AppProvider = ({ children }) => {
const applySavedFilter = filter => {
try {
const criteria=savedFilterCriteria(filter);
setDetailedQueries({institution:'',registration:''});
setSearchQuery('');setSelectedCpvler([]);setQuickFilter('all');
setDetailedQueries({institution:criteria.institutionName,registration:criteria.registration});
setSearchQuery(criteria.keyword);setSelectedCpvler([]);setQuickFilter('all');
setSelectedCity(criteria.city);setSelectedRegion(criteria.region);
setSelectedAlimTuru(criteria.procedure);setSelectedKurumTipi(criteria.institution);
setSelectedSektorler(criteria.sector?[criteria.sector]:[]);setActiveTab('list');setSaveError(null);
+1 -1
View File
@@ -15,5 +15,5 @@ export function savedFilterCriteria(filter){
const read=key=>{if(filter[key]!=null&&typeof filter[key]!=='string')throw new Error('Kayıtlı filtre ölçütü geçersiz.');return (filter[key] || '').trim();};
const region=read('bolge');
if(region&&!Object.keys(REGION_CITIES).includes(norm(region)))throw new Error('Kayıtlı bölge tanınmadı; filtreyi yeniden düzenleyin.');
return {city:read('sehir'),region:region?norm(region):'',sector:read('sektor'),institution:read('kurumTipi'),procedure:read('usul')};
return {city:read('sehir'),region:region?norm(region):'',sector:read('sektor'),institution:read('kurumTipi'),procedure:read('usul'),keyword:read('anahtarKelime'),institutionName:read('kurumAdi'),registration:read('ikn')};
}
+8 -1
View File
@@ -10,10 +10,17 @@ test('province-based regions cover all 81 cities exactly once and normalize Turk
assert.equal(matchesRegion({sehir:''},'KARADENİZ BÖLGESİ'),false);
});
test('saved criteria preserves all five fields and rejects unrecognized regions',()=>{
assert.deepEqual(savedFilterCriteria({sehir:' Giresun ',bolge:'karadeniz bölgesi',sektor:'Laboratuvar',kurumTipi:'Hastaneler',usul:'Açık İhale'}),{city:'Giresun',region:'KARADENİZ BÖLGESİ',sector:'Laboratuvar',institution:'Hastaneler',procedure:'Açık İhale'});
assert.deepEqual(savedFilterCriteria({sehir:' Giresun ',bolge:'karadeniz bölgesi',sektor:'Laboratuvar',kurumTipi:'Hastaneler',usul:'Açık İhale'}),{city:'Giresun',region:'KARADENİZ BÖLGESİ',sector:'Laboratuvar',institution:'Hastaneler',procedure:'Açık İhale',keyword:'',institutionName:'',registration:''});
assert.throws(()=>savedFilterCriteria({bolge:'Bilinmeyen'}),/tanınmadı/);
assert.throws(()=>savedFilterCriteria({sehir:[]}),/geçersiz/);
});
test('saved search text survives serialization independently and malformed values are rejected',()=>{
const criteria=savedFilterCriteria(JSON.parse(JSON.stringify({anahtarKelime:' test ürünü ',kurumAdi:' Kayseri ',ikn:' 2026/1534665 '})));
assert.equal(criteria.keyword,'test ürünü');
assert.equal(criteria.institutionName,'Kayseri');
assert.equal(criteria.registration,'2026/1534665');
for(const key of ['anahtarKelime','kurumAdi','ikn'])assert.throws(()=>savedFilterCriteria({[key]:['invalid']}),/geçersiz/);
});
test('procedure filter uses explicit source procedure, not inferred purchase category',()=>{
assert.equal(matchesProcurement({alimTuru:'Mal Alımı',ihaleUsulu:'Açık İhale'},'Açık İhale'),true);
assert.equal(matchesProcurement({alimTuru:'Mal Alımı'},'Açık İhale'),false);