fix: confirm and version favorite institution updates

This commit is contained in:
2026-09-06 14:09:31 +03:00
parent 525b673e25
commit 463fd06d30
6 changed files with 39 additions and 10 deletions
+6
View File
@@ -1010,3 +1010,9 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- İzole QA tarayıcısında altı kontrol geçti: 22 Eylül'deki 10:30 Akdeniz önce, 11:00 Kayseri sonra; Ekim girişinde 31 gün ve 1 Ekim başlığı; sonraki ay Kasım; önceki ay Ekim; bugüne dönüş; Akdeniz detayında aynı tarih/saat. Kanıt qa-calendar-navigation.json. - İzole QA tarayıcısında altı kontrol geçti: 22 Eylül'deki 10:30 Akdeniz önce, 11:00 Kayseri sonra; Ekim girişinde 31 gün ve 1 Ekim başlığı; sonraki ay Kasım; önceki ay Ekim; bugüne dönüş; Akdeniz detayında aynı tarih/saat. Kanıt qa-calendar-navigation.json.
- 228 otomatik test ve derleme geçti. Test sunucusunun çıktısında önceki firma alanı değişikliğinde yinelenmiş aria-invalid niteliği görüldü; tek niteliğe indirildi. Son derleme bu uyarı olmadan geçti; büyük paket uyarısı sürüyor. - 228 otomatik test ve derleme geçti. Test sunucusunun çıktısında önceki firma alanı değişikliğinde yinelenmiş aria-invalid niteliği görüldü; tek niteliğe indirildi. Son derleme bu uyarı olmadan geçti; büyük paket uyarısı sürüyor.
- QA sekmesi ve yalnız bu test için açılan sunucu kapatıldı (exit 0). Mobil genişlikler, tarihi bilinmeyen kaydın tarayıcı senaryosu ve Google/Outlook aktarımı bu kabul kapsamında değil; açık kalır. Canlıya yayın yapılmadı. - QA sekmesi ve yalnız bu test için açılan sunucu kapatıldı (exit 0). Mobil genişlikler, tarihi bilinmeyen kaydın tarayıcı senaryosu ve Google/Outlook aktarımı bu kabul kapsamında değil; açık kalır. Canlıya yayın yapılmadı.
### Favori kurumlarda kayıt onayı ve sürüm koruması — 6 Eylül 2026
- Favori kurumların iyimser state/effect kaydı kaldırıldı. Yıldız seçimi ancak sürümlü sunucu yazımı başarılı olunca değişir; hata mevcut listeyi korur ve ortak kayıt hata alanında gösterilir. Bekleme sırasında favori düğmeleri kilitlenir, aria-busy/aria-pressed durumları yayınlanır.
- favori_kurumlar API yazımında okunan içerik sürümü zorunlu; eski sürüm 409 alır. Liste olmayan, boş veya yinelenen kurum girdileri reddedilir. Boş listeye dönmek desteklenir.
- 229 otomatik test ve derleme geçti. HTTP testi sürüm zorunluluğu, art arda başarılı kayıt, eski sekme silme isteğinin reddi, hatalı liste ve son favoriyi kaldırmayı doğruladı.
- Yeni yıldızın tarayıcı bekleme/hata ve iki sekme kabulü açık. Bu değişiklik bildirim aboneliğini otomatik değiştirmez; abonelik kendi kaydedilmiş kurum kapsamını kullanır. Diğer genel anahtarlardan rakip izleme ve hatırlatma ayarlarının eşzamanlı kayıt davranışı ayrıca incelenmeli. Canlıya yayın yapılmadı.
+2
View File
@@ -77,3 +77,5 @@ Kuyruktaki ilanın artık kaynakta bulunmaması, teklif zamanının değişmesi,
`GET /api/me/data` artık `veri` ile aynı okumaya ait `revisions` döndürür. `firma_profil` ve `saved_filters` PUT/POST istekleri `revision` göndermelidir (yeni kayıt için null). Eksik sürüm 428, değişmiş sürüm 409 üretir; otomatik üzerine yazma veya otomatik tekrar yoktur. Frontend ve backend birlikte sürümlenmelidir. Eski açık sekme 428 alırsa kaydedilmemiş girdiler korunup sayfa yenilenmelidir. Veritabanı şeması değişmez; eski sunucuya dönmek bu iki anahtardaki çakışma korumasını kaldırır. Yayın kabulünde iki sekmenin aynı kaydı değiştirmesi ve reddedilen sekmenin girdilerinin korunması sınanmalıdır. `GET /api/me/data` artık `veri` ile aynı okumaya ait `revisions` döndürür. `firma_profil` ve `saved_filters` PUT/POST istekleri `revision` göndermelidir (yeni kayıt için null). Eksik sürüm 428, değişmiş sürüm 409 üretir; otomatik üzerine yazma veya otomatik tekrar yoktur. Frontend ve backend birlikte sürümlenmelidir. Eski açık sekme 428 alırsa kaydedilmemiş girdiler korunup sayfa yenilenmelidir. Veritabanı şeması değişmez; eski sunucuya dönmek bu iki anahtardaki çakışma korumasını kaldırır. Yayın kabulünde iki sekmenin aynı kaydı değiştirmesi ve reddedilen sekmenin girdilerinin korunması sınanmalıdır.
Ekip kaydı (`ekip`) da aynı içerik sürümü sözleşmesine dahil edildi. Yeni ekip arayüzü sunucu onayını bekler. Eski istemciden sürümsüz ekip yazımı 428 ile reddedilir; frontend/backend birlikte güncellenmelidir. Ekip alanlarının geçersiz veya kimliklerinin yinelenmiş olduğu eski kayıtlar önce düzeltilmelidir; sessiz veri temizliği yapılmaz. Ekip kaydı (`ekip`) da aynı içerik sürümü sözleşmesine dahil edildi. Yeni ekip arayüzü sunucu onayını bekler. Eski istemciden sürümsüz ekip yazımı 428 ile reddedilir; frontend/backend birlikte güncellenmelidir. Ekip alanlarının geçersiz veya kimliklerinin yinelenmiş olduğu eski kayıtlar önce düzeltilmelidir; sessiz veri temizliği yapılmaz.
Favori kurumlar (`favori_kurumlar`) da sürümlü kullanıcı verisi yazımı ister. Eski istemcinin sürümsüz yazımı 428 döner; frontend/backend birlikte sürümlenmelidir. Bekleyen değişiklikler sunucudan onay gelmeden favori listesinde gösterilmez.
+3 -1
View File
@@ -1207,7 +1207,9 @@ const server = http.createServer(async (req, res) => {
validateTeam(body.deger); validateTeam(body.deger);
kotaKontrol(user.email, 'ekip', 0, body.deger.length, { isAdmin: user.isAdmin }); kotaKontrol(user.email, 'ekip', 0, body.deger.length, { isAdmin: user.isAdmin });
} }
const protectedKey=['firma_profil','saved_filters','ekip'].includes(k); if(k==='favori_kurumlar' && (!Array.isArray(body.deger) || body.deger.some(name=>typeof name!=='string'||!name.trim()||name.length>1000) || new Set(body.deger.map(name=>name.trim())).size!==body.deger.length))
return json(res,400,{success:false,error:'Favori kurum listesi geçersiz veya yinelenen kurum içeriyor.'});
const protectedKey=['firma_profil','saved_filters','ekip','favori_kurumlar'].includes(k);
if(protectedKey && (!Object.hasOwn(body,'revision') || (body.revision!==null && (typeof body.revision!=='string' || !/^[a-f0-9]{64}$/.test(body.revision))))) if(protectedKey && (!Object.hasOwn(body,'revision') || (body.revision!==null && (typeof body.revision!=='string' || !/^[a-f0-9]{64}$/.test(body.revision)))))
return json(res,428,{success:false,kod:'VERSION_REQUIRED',error:'Güncel kayıt sürümü gerekli. Sayfayı yenileyip tekrar deneyin.'}); return json(res,428,{success:false,kod:'VERSION_REQUIRED',error:'Güncel kayıt sürümü gerekli. Sayfayı yenileyip tekrar deneyin.'});
const revision=store.setData(user.email, k, body.deger, protectedKey ? body.revision : undefined); const revision=store.setData(user.email, k, body.deger, protectedKey ? body.revision : undefined);
+3 -1
View File
@@ -3,7 +3,7 @@ import { useApp } from '../context/AppContext';
import { Star, MapPin, Clock, Tag, Package, Building2, AlertTriangle, Sparkles, CalendarDays, Megaphone } from 'lucide-react'; import { Star, MapPin, Clock, Tag, Package, Building2, AlertTriangle, Sparkles, CalendarDays, Megaphone } from 'lucide-react';
export const TenderCard = ({ tender }) => { export const TenderCard = ({ tender }) => {
const { toggleTrackTender, setActiveTenderDetail, favoriteKurumlar, toggleFavoriteKurum, kalemEslesen, searchQuery } = useApp(); const { toggleTrackTender, setActiveTenderDetail, favoriteKurumlar, favoriteSaving, toggleFavoriteKurum, kalemEslesen, searchQuery } = useApp();
// Aramada eşleşen kalemler öne alınır — firma kendi ürününü hemen görsün. // Aramada eşleşen kalemler öne alınır — firma kendi ürününü hemen görsün.
const eslesenKalemler = (kalemEslesen && tender.ikn && kalemEslesen.get(tender.ikn)) || null; const eslesenKalemler = (kalemEslesen && tender.ikn && kalemEslesen.get(tender.ikn)) || null;
const isFavKurum = favoriteKurumlar?.includes((tender.kurum || '').trim()); const isFavKurum = favoriteKurumlar?.includes((tender.kurum || '').trim());
@@ -80,6 +80,8 @@ export const TenderCard = ({ tender }) => {
<span className="truncate">{tender.kurum}</span> <span className="truncate">{tender.kurum}</span>
<button <button
onClick={(e) => { e.stopPropagation(); toggleFavoriteKurum(tender.kurum); }} onClick={(e) => { e.stopPropagation(); toggleFavoriteKurum(tender.kurum); }}
disabled={favoriteSaving || !tender.kurum?.trim()}
aria-busy={favoriteSaving} aria-pressed={Boolean(isFavKurum)}
title={isFavKurum ? 'Favori kurumdan çıkar' : 'Kurumu favorilere ekle'} title={isFavKurum ? 'Favori kurumdan çıkar' : 'Kurumu favorilere ekle'}
className="shrink-0 p-0.5" className="shrink-0 p-0.5"
> >
+15 -8
View File
@@ -357,13 +357,20 @@ export const AppProvider = ({ children }) => {
// --- Favori kurumlar (sunucu kaydı) --- // --- Favori kurumlar (sunucu kaydı) ---
const [favoriteKurumlar, setFavoriteKurumlar] = useState([]); const [favoriteKurumlar, setFavoriteKurumlar] = useState([]);
useEffect(() => { const favoriteSaveLock=useRef(false);
if (veriSenkron) ilkYazmaDegil('favori_kurumlar', favoriteKurumlar); const [favoriteSaving,setFavoriteSaving]=useState(false);
}, [favoriteKurumlar, veriSenkron]); const toggleFavoriteKurum = async kurum => {
const toggleFavoriteKurum = (kurum) => { const ad=typeof kurum==='string' ? kurum.trim() : '';
const ad = (kurum || '').trim(); if(!ad || favoriteSaveLock.current)return false;
if (!ad) return; if(!authUser||!veriSenkron){setSaveError('Hesap verileri yüklenmeden favoriler kaydedilemez.');return false;}
setFavoriteKurumlar((prev) => (prev.includes(ad) ? prev.filter((k) => k !== ad) : [...prev, ad])); const session=getToken();favoriteSaveLock.current=true;setFavoriteSaving(true);setSavingCount(n=>n+1);setSaveError(null);
const next=favoriteKurumlar.includes(ad)?favoriteKurumlar.filter(k=>k!==ad):[...favoriteKurumlar,ad];
try {
const saved=await setUserData('favori_kurumlar',next,dataRevisions.current.favori_kurumlar ?? null);
if(getToken()!==session)return false;
dataRevisions.current.favori_kurumlar=saved.revision;setFavoriteKurumlar(next);return true;
} catch(error){if(getToken()===session)setSaveError(error.message);return false;}
finally{favoriteSaveLock.current=false;setFavoriteSaving(false);setSavingCount(n=>n-1);}
}; };
// --- Ekip yönetimi (sunucu kaydı) --- // --- Ekip yönetimi (sunucu kaydı) ---
@@ -824,7 +831,7 @@ export const AppProvider = ({ children }) => {
markNotificationsRead, markNotificationsRead,
refreshNotifications,notificationError,notificationBusy, refreshNotifications,notificationError,notificationBusy,
isQuietNow, isQuietNow,
favoriteKurumlar, favoriteKurumlar,favoriteSaving,
toggleFavoriteKurum, toggleFavoriteKurum,
teamMembers,teamSaving,teamError, teamMembers,teamSaving,teamError,
addTeamMember, addTeamMember,
+10
View File
@@ -229,3 +229,13 @@ test('team membership rejects stale and over-quota writes and permits removing l
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:saved.data.revision})).status,200); assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:saved.data.revision})).status,200);
assert.deepEqual((await request('/api/me/data')).data.veri.ekip,[]); assert.deepEqual((await request('/api/me/data')).data.veri.ekip,[]);
}); });
test('favorite institutions require a version and retain current values after a stale update',async()=>{
const anahtar='favori_kurumlar';
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:['QA Kurum']})).status,428);
const first=await request('/api/me/data','PUT',{anahtar,deger:['QA Kurum'],revision:null});assert.equal(first.status,200);
const updated=await request('/api/me/data','PUT',{anahtar,deger:['QA Kurum','QA Diğer'],revision:first.data.revision});assert.equal(updated.status,200);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:first.data.revision})).status,409);
for(const bad of [{},[null],[' '],['QA Kurum',' QA Kurum ']])assert.equal((await request('/api/me/data','PUT',{anahtar,deger:bad,revision:updated.data.revision})).status,400);
assert.deepEqual((await request('/api/me/data')).data.veri[anahtar],['QA Kurum','QA Diğer']);
assert.equal((await request('/api/me/data','PUT',{anahtar,deger:[],revision:updated.data.revision})).status,200);
});