fix: isolate account state and ignore stale session responses
This commit is contained in:
+62
-97
@@ -5,7 +5,7 @@ import { getToken } from '../services/auth';
|
||||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||||
import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
|
||||
import { applyTenderStates, tenderStateKey, tenderSnapshot } from '../services/tenderState';
|
||||
import { applyTenderStates, tenderStateKey, tenderSnapshot, publicTender } from '../services/tenderState';
|
||||
import { getTenderStates, saveTenderState } from '../services/account';
|
||||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||
|
||||
@@ -25,16 +25,13 @@ const yerelKaydet = (anahtar, deger) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const yerelOku = (anahtar, varsayilan) => {
|
||||
try {
|
||||
const ham = localStorage.getItem(anahtar);
|
||||
if (!ham) return varsayilan;
|
||||
const v = JSON.parse(ham);
|
||||
return v == null ? varsayilan : v;
|
||||
} catch {
|
||||
return varsayilan;
|
||||
}
|
||||
};
|
||||
const defaultReminder = () => ({emailEnabled:true,smsEnabled:true,pushEnabled:true,
|
||||
notify3DaysBefore:true,notify1DayBefore:true,notify3HoursBefore:true,
|
||||
googleCalendarSync:true,outlookSync:false,quietEnabled:false,quietStart:'22:00',quietEnd:'08:00'});
|
||||
const defaultProfile = user => ({name:user?.name || user?.email || 'Kullanıcı',
|
||||
username:(user?.email || '').split('@')[0] || 'kullanici',company:'',website:'',trialDaysLeft:7,
|
||||
role:'Yönetici Tedarikçi',uyumEsigi:60,scrapedCapabilities:{scrapedAt:null,extractedSectors:[],
|
||||
extractedProducts:[],extractedCertifications:[],ihaleKonulari:[],matchScoreBoost:0}});
|
||||
|
||||
export const AppProvider = ({ children }) => {
|
||||
const [activeTab, setActiveTab] = useState('list'); // 'list', 'bulten', 'sonuc', 'takvim', 'sozlesme', 'kdv', 'fiyat', 'kanban', 'smart_filter_builder'
|
||||
@@ -44,7 +41,7 @@ export const AppProvider = ({ children }) => {
|
||||
try {
|
||||
const saved = localStorage.getItem(TENDER_CACHE_KEY);
|
||||
const parsed = saved ? JSON.parse(saved) : null;
|
||||
return Array.isArray(parsed) ? parsed.map(t => ({...t,takipte:false,gorev:null,notes:[],checklist:[],incelendi:false,atananPersonel:null,kanbanStatus:'incelenecek',bidDraft:null})) : [];
|
||||
return Array.isArray(parsed) ? parsed.map(publicTender) : [];
|
||||
} catch {
|
||||
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
|
||||
return [];
|
||||
@@ -107,10 +104,7 @@ export const AppProvider = ({ children }) => {
|
||||
const [onlyValidOnly, setOnlyValidOnly] = useState(true);
|
||||
const [quickFilter, setQuickFilter] = useState('all');
|
||||
|
||||
const [savedFilters, setSavedFilters] = useState(() => {
|
||||
const v = yerelOku('ihaleist_saved_filters', []);
|
||||
return Array.isArray(v) ? v : [];
|
||||
});
|
||||
const [savedFilters, setSavedFilters] = useState([]);
|
||||
|
||||
const [isLiveMode, setIsLiveMode] = useState(true);
|
||||
const [isRefreshingLive, setIsRefreshingLive] = useState(false);
|
||||
@@ -129,6 +123,12 @@ export const AppProvider = ({ children }) => {
|
||||
.finally(() => setAuthReady(true));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const sync = event => { if (event.key === 'ihaleist_user' || event.key === 'ihaleist_token') setAuthUser(getStoredUser()); };
|
||||
window.addEventListener('storage', sync);
|
||||
return () => window.removeEventListener('storage', sync);
|
||||
}, []);
|
||||
|
||||
// "Son girişten beri" kartı için önceki ziyaretin zamanı. Ziyaret = tarayıcı
|
||||
// sekmesi oturumu (sessionStorage): sayfa yenilemek sayacı sıfırlamaz, yeni
|
||||
// sekme/gün açmak önceki damgayı "önceki giriş" yapar. Eskiden sabit 14'tü.
|
||||
@@ -136,42 +136,48 @@ export const AppProvider = ({ children }) => {
|
||||
useEffect(() => {
|
||||
if (!authUser) return;
|
||||
try {
|
||||
const anahtar = 'ihaleist_son_giris';
|
||||
const anahtar = 'ihaleist_son_giris:' + authUser.email;
|
||||
const onceki = Number(localStorage.getItem(anahtar)) || null;
|
||||
const oturum = sessionStorage.getItem('ihaleist_oturum_giris');
|
||||
const oturum = sessionStorage.getItem('ihaleist_oturum_giris:' + authUser.email);
|
||||
if (oturum !== null) { // aynı sekmede yenileme: damga sabit kalır
|
||||
setOncekiGiris(Number(oturum) || null);
|
||||
return;
|
||||
}
|
||||
setOncekiGiris(onceki);
|
||||
sessionStorage.setItem('ihaleist_oturum_giris', String(onceki || 0));
|
||||
sessionStorage.setItem('ihaleist_oturum_giris:' + authUser.email, String(onceki || 0));
|
||||
localStorage.setItem(anahtar, String(Date.now()));
|
||||
} catch { /* depolama kapalıysa kart bugünü gösterir */ }
|
||||
}, [authUser?.email]);
|
||||
|
||||
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
|
||||
useEffect(() => {
|
||||
if (!authUser) { setVeriSenkron(false); setPlan(null); return; }
|
||||
setVeriSenkron(false); setPlan(null); setSaveError(null);
|
||||
setFavoriteKurumlar([]); setTeamMembers([]); setSavedFilters([]); setCompetitorWatchlist([]);
|
||||
setReminderSettings(defaultReminder()); setUserProfile(defaultProfile(authUser));
|
||||
setVaultDocuments([]); setActiveTenderDetail(null); setAiAnalysisResults(null); setNotifications([]); setOncekiGiris(null);
|
||||
setTenders(prev => prev.map(publicTender));
|
||||
if (!authUser) return;
|
||||
planYenile();
|
||||
let cancelled = false;
|
||||
setVeriSenkron(false);
|
||||
ilkYazmaAtlanan.current.clear();
|
||||
savedTenderStates.current = {};
|
||||
const loadSession = getToken();
|
||||
Promise.all([getAllUserData(), getTenderStates()])
|
||||
.then(([v, records]) => {
|
||||
if (cancelled) return;
|
||||
if (cancelled || getToken() !== loadSession) return;
|
||||
setFavoriteKurumlar(v.favori_kurumlar || []);
|
||||
setTeamMembers(v.ekip || []);
|
||||
setSavedFilters(v.saved_filters || []);
|
||||
if (v.reminder) setReminderSettings((s) => ({ ...s, ...v.reminder }));
|
||||
setReminderSettings({...defaultReminder(), ...v.reminder});
|
||||
setCompetitorWatchlist(v.rakip_izleme || []);
|
||||
if (v.firma_profil) setUserProfile(p => ({...p,...v.firma_profil}));
|
||||
setUserProfile({...defaultProfile(authUser), ...v.firma_profil});
|
||||
savedTenderStates.current = records;
|
||||
setTenders(prev => applyTenderStates(prev, records));
|
||||
setTenders(prev => applyTenderStates(prev.map(publicTender), records));
|
||||
setVeriSenkron(true);
|
||||
setSaveError(null);
|
||||
})
|
||||
.catch((e) => { if (!cancelled) setSaveError('Hesap verileri yüklenemedi: ' + e.message); });
|
||||
.catch((e) => { if (!cancelled && getToken() === loadSession) setSaveError('Hesap verileri yüklenemedi: ' + e.message); });
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authUser?.email]);
|
||||
@@ -190,21 +196,23 @@ export const AppProvider = ({ children }) => {
|
||||
const key = tenderStateKey(t);
|
||||
if (inFlight.current.has(key)) return false;
|
||||
const owner = authUser.email;
|
||||
const session = getToken();
|
||||
inFlight.current.add(key); setSavingCount(n => n + 1); setSaveError(null);
|
||||
try {
|
||||
const record = await saveTenderState(key, patch, tenderSnapshot(t), savedTenderStates.current[key]?.version || 0);
|
||||
if (getStoredUser()?.email !== owner) return false;
|
||||
if (getStoredUser()?.email !== owner || getToken() !== session) return false;
|
||||
savedTenderStates.current = {...savedTenderStates.current,[key]:record};
|
||||
setTenders(prev => applyTenderStates(prev, savedTenderStates.current));
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (getStoredUser()?.email !== owner || getToken() !== session) return false;
|
||||
setSaveError(e.message);
|
||||
if (e.kod === 'VERSION_CONFLICT') {
|
||||
try {
|
||||
const records = await getTenderStates();
|
||||
if (getStoredUser()?.email === owner) {
|
||||
if (getStoredUser()?.email === owner && getToken() === session) {
|
||||
savedTenderStates.current = records;
|
||||
setTenders(prev => applyTenderStates(prev, records));
|
||||
setTenders(prev => applyTenderStates(prev.map(publicTender), records));
|
||||
}
|
||||
} catch { /* retain visible error */ }
|
||||
}
|
||||
@@ -223,15 +231,17 @@ export const AppProvider = ({ children }) => {
|
||||
|
||||
|
||||
const sunucuyaYaz = (anahtar, deger) => {
|
||||
if (!authUser) return;
|
||||
if (!authUser || !veriSenkron || getStoredUser()?.email !== authUser.email) return;
|
||||
const session = getToken();
|
||||
setUserData(anahtar, deger).catch((e) => {
|
||||
if (getToken() !== session) return;
|
||||
if (e.kod === 'KOTA_ASILDI') setKotaUyarisi(e.message);
|
||||
else setSaveError('Kaydedilemedi: ' + e.message);
|
||||
});
|
||||
};
|
||||
const [kotaUyarisi, setKotaUyarisi] = useState(null);
|
||||
|
||||
const planYenile = () => getPlan().then(setPlan).catch(() => {});
|
||||
const planYenile = () => { const session=getToken(); return getPlan().then(p => {if(getToken()===session)setPlan(p);}).catch(() => {}); };
|
||||
|
||||
const handleLogin = (user) => setAuthUser(user);
|
||||
const handleLogout = () => {
|
||||
@@ -303,21 +313,15 @@ export const AppProvider = ({ children }) => {
|
||||
const [aiAnalyzing, setAiAnalyzing] = useState(false);
|
||||
|
||||
// Enterprise Module States
|
||||
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (localStorage)
|
||||
const [competitorWatchlist, setCompetitorWatchlist] = useState(() => {
|
||||
try { return JSON.parse(localStorage.getItem('ihaleist_rakip_izleme') || '[]'); } catch { return []; }
|
||||
});
|
||||
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (hesaba özel sunucu kaydı)
|
||||
const [competitorWatchlist, setCompetitorWatchlist] = useState([]);
|
||||
useEffect(() => {
|
||||
yerelKaydet('ihaleist_rakip_izleme', competitorWatchlist);
|
||||
if (veriSenkron) ilkYazmaDegil('rakip_izleme', competitorWatchlist);
|
||||
}, [competitorWatchlist, veriSenkron]);
|
||||
|
||||
// --- Favori kurumlar (localStorage, gerçek) ---
|
||||
const [favoriteKurumlar, setFavoriteKurumlar] = useState(() => {
|
||||
try { return JSON.parse(localStorage.getItem('ihaleist_favori_kurumlar') || '[]'); } catch { return []; }
|
||||
});
|
||||
// --- Favori kurumlar (sunucu kaydı) ---
|
||||
const [favoriteKurumlar, setFavoriteKurumlar] = useState([]);
|
||||
useEffect(() => {
|
||||
yerelKaydet('ihaleist_favori_kurumlar', favoriteKurumlar);
|
||||
if (veriSenkron) ilkYazmaDegil('favori_kurumlar', favoriteKurumlar);
|
||||
}, [favoriteKurumlar, veriSenkron]);
|
||||
const toggleFavoriteKurum = (kurum) => {
|
||||
@@ -326,44 +330,19 @@ export const AppProvider = ({ children }) => {
|
||||
setFavoriteKurumlar((prev) => (prev.includes(ad) ? prev.filter((k) => k !== ad) : [...prev, ad]));
|
||||
};
|
||||
|
||||
// --- Ekip yönetimi (localStorage, gerçek CRUD) ---
|
||||
const [teamMembers, setTeamMembers] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('ihaleist_ekip');
|
||||
if (saved) return JSON.parse(saved);
|
||||
} catch { /* yoksay */ }
|
||||
const u = getStoredUser();
|
||||
return u ? [{ id: 'tm-1', name: u.name || u.email, email: u.email || '', role: 'Yönetici' }] : [];
|
||||
});
|
||||
// --- Ekip yönetimi (sunucu kaydı) ---
|
||||
const [teamMembers, setTeamMembers] = useState([]);
|
||||
useEffect(() => {
|
||||
yerelKaydet('ihaleist_ekip', teamMembers);
|
||||
if (veriSenkron) ilkYazmaDegil('ekip', teamMembers);
|
||||
}, [teamMembers, veriSenkron]);
|
||||
const addTeamMember = (m) => setTeamMembers((prev) => [...prev, { id: 'tm-' + Date.now(), ...m }]);
|
||||
const removeTeamMember = (id) => setTeamMembers((prev) => prev.filter((m) => m.id !== id));
|
||||
|
||||
// Belge kasası — kullanıcı kendi belgelerini ekler (localStorage)
|
||||
const [vaultDocuments, setVaultDocuments] = useState(() => {
|
||||
try { return JSON.parse(localStorage.getItem('ihaleist_belgeler') || '[]'); } catch { return []; }
|
||||
});
|
||||
useEffect(() => {
|
||||
yerelKaydet('ihaleist_belgeler', vaultDocuments);
|
||||
}, [vaultDocuments]);
|
||||
// Belge kasası — kullanıcı kendi belgelerini ekler (hesaba özel sunucu kaydı)
|
||||
const [vaultDocuments, setVaultDocuments] = useState([]);
|
||||
|
||||
const [reminderSettings, setReminderSettings] = useState(() => {
|
||||
const def = {
|
||||
emailEnabled: true, smsEnabled: true, pushEnabled: true,
|
||||
notify3DaysBefore: true, notify1DayBefore: true, notify3HoursBefore: true,
|
||||
googleCalendarSync: true, outlookSync: false,
|
||||
quietEnabled: false, quietStart: '22:00', quietEnd: '08:00',
|
||||
};
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem('ihaleist_reminder') || 'null');
|
||||
return saved ? { ...def, ...saved } : def;
|
||||
} catch { return def; }
|
||||
});
|
||||
const [reminderSettings, setReminderSettings] = useState(defaultReminder);
|
||||
useEffect(() => {
|
||||
yerelKaydet('ihaleist_reminder', reminderSettings);
|
||||
if (veriSenkron) ilkYazmaDegil('reminder', reminderSettings);
|
||||
}, [reminderSettings, veriSenkron]);
|
||||
|
||||
@@ -441,6 +420,7 @@ export const AppProvider = ({ children }) => {
|
||||
// seçilen model zinciri (fallback) ile.
|
||||
// mode='derin' → ince detaylar, belge kontrol listesi ve yol haritası da üretilir
|
||||
const analyzeSpecPdfWithGemini = async ({ text, fileName, tender, mode } = {}) => {
|
||||
const session = getToken();
|
||||
setAiAnalyzing(true);
|
||||
setAiAnalysisResults(null);
|
||||
try {
|
||||
@@ -457,6 +437,7 @@ export const AppProvider = ({ children }) => {
|
||||
attempts: data.attempts,
|
||||
analyzedAt: new Date(data.analyzedAt).toLocaleString('tr-TR'),
|
||||
};
|
||||
if(getToken() !== session) throw new Error('Oturum değişti; analiz sonucu bu hesaba aktarılmadı.');
|
||||
setAiAnalysisResults(result);
|
||||
return result;
|
||||
} finally {
|
||||
@@ -464,33 +445,12 @@ export const AppProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const BOS_YETKINLIK = {
|
||||
scrapedAt: null, extractedSectors: [], extractedProducts: [], extractedCertifications: [],
|
||||
ihaleKonulari: [], matchScoreBoost: 0,
|
||||
};
|
||||
const [userProfile, setUserProfile] = useState(() => {
|
||||
// Yetkinlikler uyum skorunun girdisi — sayfa yenilenince sıfırlanmasın
|
||||
let kayitli = null;
|
||||
try { kayitli = JSON.parse(localStorage.getItem('ihaleist_firma_profil') || 'null'); } catch { /* yoksay */ }
|
||||
const u = getStoredUser();
|
||||
return {
|
||||
name: u?.name || u?.email || 'Kullanıcı',
|
||||
username: (u?.email || '').split('@')[0] || 'kullanici',
|
||||
company: kayitli?.company || '',
|
||||
website: kayitli?.website || '',
|
||||
trialDaysLeft: 7,
|
||||
role: 'Yönetici Tedarikçi',
|
||||
scrapedCapabilities: { ...BOS_YETKINLIK, ...(kayitli?.scrapedCapabilities || {}) },
|
||||
// Firma eşiği: "uyumlu" filtresi ve bildirim bu skordan itibaren
|
||||
uyumEsigi: Number.isFinite(kayitli?.uyumEsigi) ? kayitli.uyumEsigi : 60,
|
||||
};
|
||||
});
|
||||
const [userProfile, setUserProfile] = useState(() => defaultProfile(getStoredUser()));
|
||||
useEffect(() => {
|
||||
const veri = {
|
||||
company: userProfile.company, website: userProfile.website,
|
||||
scrapedCapabilities: userProfile.scrapedCapabilities, uyumEsigi: userProfile.uyumEsigi,
|
||||
};
|
||||
yerelKaydet('ihaleist_firma_profil', veri);
|
||||
if (veriSenkron) ilkYazmaDegil('firma_profil', veri);
|
||||
}, [userProfile.company, userProfile.website, userProfile.scrapedCapabilities, userProfile.uyumEsigi, veriSenkron]);
|
||||
const setUyumEsigi = (v) => setUserProfile((p) => ({ ...p, uyumEsigi: Math.max(0, Math.min(100, Number(v) || 0)) }));
|
||||
@@ -532,6 +492,7 @@ export const AppProvider = ({ children }) => {
|
||||
// Önce gerçek tarama (/api/company/scan: site metni + model), başarısızsa
|
||||
// alan adından tahmin eden eski yol. Sonuç uyum skorunun girdisi olur.
|
||||
const scanCompanyWebsite = async (url) => {
|
||||
const session = getToken();
|
||||
let yeni = null;
|
||||
try {
|
||||
const r = await fetch('/api/company/scan', {
|
||||
@@ -556,6 +517,7 @@ export const AppProvider = ({ children }) => {
|
||||
}
|
||||
} catch { /* aşağıda yedek yol */ }
|
||||
if (!yeni) yeni = { ...parseWebsiteCapabilitiesDynamically(url), ihaleKonulari: [], gercekTarama: false };
|
||||
if(getToken() !== session) throw new Error('Oturum değişti; firma taraması bu hesaba aktarılmadı.');
|
||||
setUserProfile((prev) => ({ ...prev, website: url, scrapedCapabilities: yeni }));
|
||||
return yeni;
|
||||
};
|
||||
@@ -596,7 +558,7 @@ export const AppProvider = ({ children }) => {
|
||||
// boyamaya yetecek kadarını, hataya dayanıklı biçimde saklıyoruz.
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(TENDER_CACHE_KEY, JSON.stringify(tenders.slice(0, TENDER_CACHE_LIMIT)));
|
||||
localStorage.setItem(TENDER_CACHE_KEY, JSON.stringify(tenders.slice(0, TENDER_CACHE_LIMIT).map(publicTender)));
|
||||
} catch {
|
||||
// Kota dolduysa önbelleği bırak — veri zaten sunucudan geliyor.
|
||||
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
|
||||
@@ -604,7 +566,6 @@ export const AppProvider = ({ children }) => {
|
||||
}, [tenders]);
|
||||
|
||||
useEffect(() => {
|
||||
yerelKaydet('ihaleist_saved_filters', savedFilters);
|
||||
if (veriSenkron) ilkYazmaDegil('saved_filters', savedFilters);
|
||||
}, [savedFilters, veriSenkron]);
|
||||
|
||||
@@ -654,11 +615,12 @@ export const AppProvider = ({ children }) => {
|
||||
useEffect(() => {
|
||||
if (!veriSenkron) return;
|
||||
let iptal = false;
|
||||
const session = getToken();
|
||||
const cek = async () => {
|
||||
try {
|
||||
const liste = await bildirimleriGetir();
|
||||
const yeni = liste.filter((b) => !b.okundu);
|
||||
if (iptal || !yeni.length) return;
|
||||
if (iptal || getToken() !== session || !yeni.length) return;
|
||||
for (const b of yeni.slice(0, 5)) notify(b.baslik, b.metin);
|
||||
await bildirimOkundu(yeni.map((b) => b.id));
|
||||
} catch { /* sessiz */ }
|
||||
@@ -674,9 +636,12 @@ export const AppProvider = ({ children }) => {
|
||||
// fark etmiyor — fikir: MOBIT-DURT).
|
||||
useEffect(() => {
|
||||
if (!veriSenkron) return;
|
||||
const session = getToken();
|
||||
const uyariAnahtari = 'ihaleist_belge_uyari_gunu:' + authUser?.email;
|
||||
const bugun = new Date().toISOString().slice(0, 10);
|
||||
try { if (localStorage.getItem('ihaleist_belge_uyari_gunu') === bugun) return; } catch { /* yoksay */ }
|
||||
try { if (localStorage.getItem(uyariAnahtari) === bugun) return; } catch { /* yoksay */ }
|
||||
listDocuments().then((d) => {
|
||||
if(getToken() !== session) return;
|
||||
const simdi = Date.now();
|
||||
const dolan = [], yakin = [];
|
||||
for (const b of d?.belgeler || []) {
|
||||
@@ -686,7 +651,7 @@ export const AppProvider = ({ children }) => {
|
||||
}
|
||||
if (dolan.length) notify(`${dolan.length} belgenin süresi dolmuş`, dolan.slice(0, 4).join(', '));
|
||||
if (yakin.length) notify(`${yakin.length} belgenin süresi 30 gün içinde doluyor`, yakin.slice(0, 4).join(', '));
|
||||
try { localStorage.setItem('ihaleist_belge_uyari_gunu', bugun); } catch { /* yoksay */ }
|
||||
try { localStorage.setItem(uyariAnahtari, bugun); } catch { /* yoksay */ }
|
||||
}).catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [veriSenkron]);
|
||||
|
||||
Reference in New Issue
Block a user