Wait for saved filter persistence before reporting success
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-09-06",
|
||||||
|
"environment": "localhost:5182 synthetic local account",
|
||||||
|
"checks": {
|
||||||
|
"saveAcknowledged": true,
|
||||||
|
"reloadPreserved": true,
|
||||||
|
"deleted": true,
|
||||||
|
"deletePersisted": true
|
||||||
|
},
|
||||||
|
"limits": [
|
||||||
|
"Save, reload, delete and reload tested",
|
||||||
|
"No save failure injection or multiple-device edit test",
|
||||||
|
"Saved-filter application to search criteria remains to be implemented",
|
||||||
|
"No notification subscription created"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -911,3 +911,9 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
|
|||||||
- İlan metnindeki ilk 3000 karakter sınırı kaldırıldı. Daha sonraki ürün ifadeleri de ilan metni nedeniyle eşleşebilir.
|
- İlan metnindeki ilk 3000 karakter sınırı kaldırıldı. Daha sonraki ürün ifadeleri de ilan metni nedeniyle eşleşebilir.
|
||||||
- 206 otomatik test ve derleme geçti. Portföy-only profil, açıklama kaynağı, ilgisiz akaryakıt örneğinin sıfır kalması, ÜTS/kapasitenin puan terimi olmaması, uzun metinde geç eşleşme, çift puan engeli ve portföy kaldırılınca eski puanın temizlenmesi doğrulandı.
|
- 206 otomatik test ve derleme geçti. Portföy-only profil, açıklama kaynağı, ilgisiz akaryakıt örneğinin sıfır kalması, ÜTS/kapasitenin puan terimi olmaması, uzun metinde geç eşleşme, çift puan engeli ve portföy kaldırılınca eski puanın temizlenmesi doğrulandı.
|
||||||
- Bu testler eşleşme bağlantısını kanıtlar; yüksek skor teknik yeterlilik veya gerçek teklif uygunluğu değildir. Büyük havuz performansı ve gerçek portföy/ilan örneklerinde eşleşme kalitesi ayrıca kabul edilmelidir.
|
- Bu testler eşleşme bağlantısını kanıtlar; yüksek skor teknik yeterlilik veya gerçek teklif uygunluğu değildir. Büyük havuz performansı ve gerçek portföy/ilan örneklerinde eşleşme kalitesi ayrıca kabul edilmelidir.
|
||||||
|
|
||||||
|
### Kayıtlı filtrelerde gerçek kayıt sonucu — 6 Eylül 2026
|
||||||
|
- Filtre oluşturma/silme daha önce yerel listeyi hemen değiştirip arka plandaki yazmayı beklemiyordu. saved_filters autosave kaldırıldı; ortak kayıt işlemi sunucu yanıtından sonra listeyi günceller. Kilit, hesap/oturum kontrolü, kayıt durumu ve hata gösterimi eklendi. Başarısız kayıtta isim temizlenmez; silme başarısızsa mevcut liste korunur.
|
||||||
|
- Bildirim aboneliği oluşturmayan işlemdeki anında haber/bot çalıştırma iddiaları kaldırıldı. E-posta aboneliğinin ayrı kurulduğu açıklanır. Boş isim için alert yerine native required kullanılır; form alanları ve silme düğmeleri etiketlendi.
|
||||||
|
- 206 test ve derleme geçti. Yerel tarayıcıda kaydetme onayı, yenilemede kayıt, silme ve yenilemede silinmiş kalma olmak üzere 4 kontrol geçti (qa-saved-filter-lifecycle.json). Bu tur kayıt hatası enjeksiyonu yapılmadı; çok cihazlı eşzamanlı düzenleme kabulü açık.
|
||||||
|
- Ayrı eksik: kayıtlı filtre kartını arama ölçütlerine uygulama akışı mevcut değildi ve bu tur eklenmedi; bölge/şehir/sektör/usul ölçütleriyle uygulama sonraki iş olarak açık. Filtre yönetimi henüz tam uçtan uca kabul edilmiş sayılmaz.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const LeftSidebar = ({onOpenProductSearch} = {}) => {
|
|||||||
setActiveTab,
|
setActiveTab,
|
||||||
savedFilters,
|
savedFilters,
|
||||||
deleteSavedFilter,
|
deleteSavedFilter,
|
||||||
|
savingCount,
|
||||||
selectedAlimTuru,
|
selectedAlimTuru,
|
||||||
setSelectedAlimTuru,
|
setSelectedAlimTuru,
|
||||||
selectedSektorler,
|
selectedSektorler,
|
||||||
@@ -138,7 +139,7 @@ export const LeftSidebar = ({onOpenProductSearch} = {}) => {
|
|||||||
<Bot className="w-4 h-4 text-blue-600" /> Kayıtlı Filtrelerim
|
<Bot className="w-4 h-4 text-blue-600" /> Kayıtlı Filtrelerim
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] bg-blue-100 text-blue-800 font-bold px-2 py-0.5 rounded-full">
|
<span className="text-[10px] bg-blue-100 text-blue-800 font-bold px-2 py-0.5 rounded-full">
|
||||||
{savedFilters.length} Bot
|
{savedFilters.length} filtre
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ export const LeftSidebar = ({onOpenProductSearch} = {}) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteSavedFilter(filter.id)}
|
disabled={savingCount > 0} aria-label={`${filter.name} filtresini sil`} onClick={() => deleteSavedFilter(filter.id)}
|
||||||
title="Filtreyi Sil"
|
title="Filtreyi Sil"
|
||||||
className="text-slate-400 hover:text-red-600 p-1 opacity-60 group-hover:opacity-100 transition-opacity"
|
className="text-slate-400 hover:text-red-600 p-1 opacity-60 group-hover:opacity-100 transition-opacity"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/constants';
|
|||||||
import { Bot, ArrowLeft, CheckCircle2, HelpCircle, Building2, MapPin, Tag } from 'lucide-react';
|
import { Bot, ArrowLeft, CheckCircle2, HelpCircle, Building2, MapPin, Tag } from 'lucide-react';
|
||||||
|
|
||||||
export const SmartFilterBuilder = () => {
|
export const SmartFilterBuilder = () => {
|
||||||
const { savedFilters, addSavedFilter, deleteSavedFilter, setActiveTab, sektorListesi } = useApp();
|
const { savedFilters, addSavedFilter, deleteSavedFilter, setActiveTab, sektorListesi, savingCount, saveError } = useApp();
|
||||||
|
|
||||||
const [filterName, setFilterName] = useState('');
|
const [filterName, setFilterName] = useState('');
|
||||||
const [selectedKurumTip, setSelectedKurumTip] = useState('');
|
const [selectedKurumTip, setSelectedKurumTip] = useState('');
|
||||||
@@ -23,14 +23,15 @@ export const SmartFilterBuilder = () => {
|
|||||||
'Açık İhale', 'Dış Alım', 'Doğrudan Temin', 'İstisna', 'Pazarlık Usulü', 'Yaklaşık Maliyet'
|
'Açık İhale', 'Dış Alım', 'Doğrudan Temin', 'İstisna', 'Pazarlık Usulü', 'Yaklaşık Maliyet'
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleCreateFilter = (e) => {
|
const handleCreateFilter = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!filterName.trim()) {
|
if (!filterName.trim()) {
|
||||||
alert('Lütfen bir filtre adı giriniz.');
|
setSuccessMessage('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addSavedFilter({
|
setSuccessMessage('');
|
||||||
|
const saved=await addSavedFilter({
|
||||||
name: filterName.trim(),
|
name: filterName.trim(),
|
||||||
kurumTipi: selectedKurumTip,
|
kurumTipi: selectedKurumTip,
|
||||||
usul: selectedUsul,
|
usul: selectedUsul,
|
||||||
@@ -39,9 +40,10 @@ export const SmartFilterBuilder = () => {
|
|||||||
sektor: selectedSektor
|
sektor: selectedSektor
|
||||||
});
|
});
|
||||||
|
|
||||||
setSuccessMessage('Yeni akıllı filtre başarıyla oluşturuldu.');
|
if(!saved)return;
|
||||||
|
setSuccessMessage('Filtre sunucuya kaydedildi. Bu kayıt bildirim aboneliği oluşturmaz.');
|
||||||
setFilterName('');
|
setFilterName('');
|
||||||
setTimeout(() => setSuccessMessage(''), 4000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,8 +56,8 @@ export const SmartFilterBuilder = () => {
|
|||||||
<Bot className="w-6 h-6" />
|
<Bot className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-black text-slate-900">Akıllı Filtre (Bot) Kurulumu</h1>
|
<h1 className="text-xl font-black text-slate-900">Kayıtlı Filtreler</h1>
|
||||||
<p className="text-xs text-slate-500">Sektör veya şehrinize uygun yeni ihale eklendiğinde anında haberiniz olsun.</p>
|
<p className="text-xs text-slate-500">Arama ölçütlerinizi hesabınıza kaydedin. E-posta bildirimleri Yönetim → Mobil PWA bölümünden ayrıca kurulur.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -63,12 +65,14 @@ export const SmartFilterBuilder = () => {
|
|||||||
className="bg-slate-800 hover:bg-slate-700 text-white font-semibold text-xs px-4 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-xs"
|
className="bg-slate-800 hover:bg-slate-700 text-white font-semibold text-xs px-4 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-xs"
|
||||||
>
|
>
|
||||||
<HelpCircle className="w-4 h-4" />
|
<HelpCircle className="w-4 h-4" />
|
||||||
<span>Nasıl Çalışır?</span>
|
<span>İhale listesine dön</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{savingCount > 0 && <p role="status" className="text-sm">Filtre işlemi kaydediliyor…</p>}
|
||||||
|
{saveError && <p role="alert" className="text-sm text-red-700">{saveError}</p>}
|
||||||
{successMessage && (
|
{successMessage && (
|
||||||
<div className="bg-emerald-50 border border-emerald-200 text-emerald-800 px-4 py-3 rounded-xl text-xs font-semibold flex items-center gap-2 animate-fade-in shadow-xs">
|
<div role="status" className="bg-emerald-50 border border-emerald-200 text-emerald-800 px-4 py-3 rounded-xl text-xs font-semibold flex items-center gap-2 animate-fade-in shadow-xs">
|
||||||
<CheckCircle2 className="w-5 h-5 text-emerald-600 shrink-0" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-600 shrink-0" />
|
||||||
<span>{successMessage}</span>
|
<span>{successMessage}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +98,7 @@ export const SmartFilterBuilder = () => {
|
|||||||
<div className="flex items-center justify-between font-bold text-slate-900">
|
<div className="flex items-center justify-between font-bold text-slate-900">
|
||||||
<span>🤖 {f.name}</span>
|
<span>🤖 {f.name}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteSavedFilter(f.id)}
|
disabled={savingCount > 0} aria-label={`${f.name} filtresini sil`} onClick={async()=>{setSuccessMessage('');await deleteSavedFilter(f.id);}}
|
||||||
className="text-slate-400 hover:text-red-600 text-xs"
|
className="text-slate-400 hover:text-red-600 text-xs"
|
||||||
>
|
>
|
||||||
🗑️
|
🗑️
|
||||||
@@ -116,7 +120,7 @@ export const SmartFilterBuilder = () => {
|
|||||||
|
|
||||||
{/* Right 3 Columns: Filter Form */}
|
{/* Right 3 Columns: Filter Form */}
|
||||||
<div className="lg:col-span-3 space-y-6">
|
<div className="lg:col-span-3 space-y-6">
|
||||||
<form onSubmit={handleCreateFilter} className="space-y-6">
|
<form onSubmit={handleCreateFilter}><fieldset disabled={savingCount > 0} className="space-y-6">
|
||||||
|
|
||||||
{/* Filter Name Card */}
|
{/* Filter Name Card */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-5 space-y-2">
|
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-5 space-y-2">
|
||||||
@@ -126,12 +130,13 @@ export const SmartFilterBuilder = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
aria-label="Filtre adı"
|
||||||
value={filterName}
|
value={filterName}
|
||||||
onChange={(e) => setFilterName(e.target.value)}
|
onChange={(e) => setFilterName(e.target.value)}
|
||||||
placeholder="Örn: Ege Bölgesi Cihaz İhaleleri"
|
placeholder="Örn: Ege Bölgesi Cihaz İhaleleri"
|
||||||
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-4 py-2.5 text-sm text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-4 py-2.5 text-sm text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-slate-400">Bu isim sadece sizin takip ve bildirim akışınızda gösterilecektir.</p>
|
<p className="text-[11px] text-slate-400">Bu ad kayıtlı filtre listenizde görünür.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle Grid */}
|
{/* Middle Grid */}
|
||||||
@@ -205,6 +210,7 @@ export const SmartFilterBuilder = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">BÖLGELER</label>
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5">BÖLGELER</label>
|
||||||
<select
|
<select
|
||||||
|
aria-label="Filtre bölgesi"
|
||||||
value={selectedRegion}
|
value={selectedRegion}
|
||||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||||
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
|
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
|
||||||
@@ -219,6 +225,7 @@ export const SmartFilterBuilder = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">ŞEHİRLER</label>
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5">ŞEHİRLER</label>
|
||||||
<select
|
<select
|
||||||
|
aria-label="Filtre şehri"
|
||||||
value={selectedCity}
|
value={selectedCity}
|
||||||
onChange={(e) => setSelectedCity(e.target.value)}
|
onChange={(e) => setSelectedCity(e.target.value)}
|
||||||
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
|
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
|
||||||
@@ -233,6 +240,7 @@ export const SmartFilterBuilder = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-600 mb-1.5">SEKTÖR</label>
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5">SEKTÖR</label>
|
||||||
<select
|
<select
|
||||||
|
aria-label="Filtre sektörü"
|
||||||
value={selectedSektor}
|
value={selectedSektor}
|
||||||
onChange={(e) => setSelectedSektor(e.target.value)}
|
onChange={(e) => setSelectedSektor(e.target.value)}
|
||||||
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
|
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
|
||||||
@@ -261,11 +269,11 @@ export const SmartFilterBuilder = () => {
|
|||||||
className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-6 py-2.5 rounded-lg text-xs transition-colors shadow-md flex items-center gap-2"
|
className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-6 py-2.5 rounded-lg text-xs transition-colors shadow-md flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Bot className="w-4 h-4" />
|
<Bot className="w-4 h-4" />
|
||||||
<span>Akıllı Filtre Botunu Kaydet & Çalıştır</span>
|
<span>Filtreyi kaydet</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</fieldset></form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-14
@@ -593,9 +593,6 @@ export const AppProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [tenders]);
|
}, [tenders]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (veriSenkron) ilkYazmaDegil('saved_filters', savedFilters);
|
|
||||||
}, [savedFilters, veriSenkron]);
|
|
||||||
|
|
||||||
// (Sahte "online kullanıcı" rastgele sayacı kaldırıldı — canlı istatistikler
|
// (Sahte "online kullanıcı" rastgele sayacı kaldırıldı — canlı istatistikler
|
||||||
// gerçek /api/stats verisinden gelir.)
|
// gerçek /api/stats verisinden gelir.)
|
||||||
@@ -677,18 +674,20 @@ export const AppProvider = ({ children }) => {
|
|||||||
return mutateTender(tenderId, {kanbanStatus:newStatus,atananPersonel:assignedUser ?? t?.atananPersonel ?? null,takipte:true});
|
return mutateTender(tenderId, {kanbanStatus:newStatus,atananPersonel:assignedUser ?? t?.atananPersonel ?? null,takipte:true});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSavedFilter = (newFilter) => {
|
const filterSaveLock=useRef(false);
|
||||||
const filterObj = {
|
const persistSavedFilters=async next=>{
|
||||||
id: 'sf-' + Date.now(),
|
if(!authUser||!veriSenkron){setSaveError('Hesap verileri yüklenmeden filtre kaydedilemez.');return false;}
|
||||||
user: userProfile.username,
|
if(filterSaveLock.current){setSaveError('Önceki filtre kaydı sürüyor.');return false;}
|
||||||
...newFilter
|
const session=getToken();filterSaveLock.current=true;setSavingCount(n=>n+1);setSaveError(null);
|
||||||
};
|
try{
|
||||||
setSavedFilters(prev => [filterObj, ...prev]);
|
await setUserData('saved_filters',next);
|
||||||
};
|
if(getToken()!==session)return false;
|
||||||
|
setSavedFilters(next);return true;
|
||||||
const deleteSavedFilter = (id) => {
|
}catch(error){if(getToken()===session)setSaveError(error.message);return false;}
|
||||||
setSavedFilters(prev => prev.filter(f => f.id !== id));
|
finally{filterSaveLock.current=false;setSavingCount(n=>n-1);}
|
||||||
};
|
};
|
||||||
|
const addSavedFilter = newFilter => persistSavedFilters([{...newFilter,id:'sf-'+crypto.randomUUID(),user:userProfile.username},...savedFilters]);
|
||||||
|
const deleteSavedFilter = id => persistSavedFilters(savedFilters.filter(filter=>filter.id!==id));
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
const clearAllFilters = () => {
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
|
|||||||
Reference in New Issue
Block a user