fix: confirm and version favorite institution updates
This commit is contained in:
@@ -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ı.
|
||||||
|
|||||||
@@ -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
@@ -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,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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user