875 lines
42 KiB
React
875 lines
42 KiB
React
import {validateReminderPreferences,quietNow} from '../services/reminderPreferences';
|
||
import {validateTeam} from '../services/teamData';
|
||
import {savedFilterCriteria} from '../services/savedFilterCriteria';
|
||
import {mergeServerNotifications,acknowledgeNotifications,addLocalNotification} from '../services/notifications';
|
||
import { searchTender } from '../services/searchTender';
|
||
import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch';
|
||
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
|
||
import { scanCompanyProposal } from '../services/companyProfile';
|
||
import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum';
|
||
import { getToken } from '../services/auth';
|
||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||
import { getUserDataSnapshot, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
|
||
import { applyTenderStates, tenderStateKey, tenderSnapshot, publicTender, tenderSourceChanges } from '../services/tenderState';
|
||
import { getTenderStates, saveTenderState } from '../services/account';
|
||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||
|
||
const AppContext = createContext();
|
||
|
||
// İhale önbelleği: tam havuz (10.000+ kayıt) localStorage kotasına sığmaz.
|
||
const TENDER_CACHE_KEY = 'ihaleist_tenders';
|
||
const TENDER_CACHE_LIMIT = 300;
|
||
|
||
// localStorage her zaman yazılabilir değildir (kota dolu, gizli sekme, site
|
||
// verisi kapalı). Yazma hatası uygulamayı çökertmemeli.
|
||
const yerelKaydet = (anahtar, deger) => {
|
||
try {
|
||
localStorage.setItem(anahtar, JSON.stringify(deger));
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
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'
|
||
const [tenders, setTenders] = useState(() => {
|
||
// Sadece ilk boyama için küçük bir önbellek; asıl kaynak sunucudur.
|
||
// Bozuk/eski kayıt uygulamayı çökertmemeli.
|
||
try {
|
||
const saved = localStorage.getItem(TENDER_CACHE_KEY);
|
||
const parsed = saved ? JSON.parse(saved) : null;
|
||
return Array.isArray(parsed) ? parsed.map(publicTender) : [];
|
||
} catch {
|
||
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
|
||
return [];
|
||
}
|
||
});
|
||
|
||
const [liveStats, setLiveStats] = useState({ toplamIhale: 0, bugunEklenen: 0, arsiv: 0 });
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [detailedQueries,setDetailedQueries]=useState({institution:'',registration:''});
|
||
|
||
// Kalem havuzunda arama: firmanın kendi ürününü ("eldiven", "ortopedik vida")
|
||
// hangi ihalelerin teklif cetvelinde geçtiğini bulması için. Kartta yalnızca
|
||
// ilk birkaç kalem taşınıyor, tam arama sunucuda yapılıyor.
|
||
const [kalemSonucu, setKalemSonucu] = useState(null);
|
||
const kalemEslesen = matchingItemResults(kalemSonucu, searchQuery);
|
||
const [kalemRetry,setKalemRetry] = useState(0);
|
||
const retryItemSearch = () => setKalemRetry(n=>n+1);
|
||
const itemSearchState = kalemSonucu?.query === normalizeTenderSearch(searchQuery) ? kalemSonucu : null;
|
||
useEffect(() => {
|
||
const q = searchQuery.trim();
|
||
if (q.length < 2) { setKalemSonucu(null); return; }
|
||
let iptal = false;
|
||
const controller=new AbortController();
|
||
setKalemSonucu({query:normalizeTenderSearch(q),status:'loading',matches:null});
|
||
const zaman = setTimeout(() => {
|
||
import('../services/kalem')
|
||
.then(({ kalemAra }) => kalemAra(q,{signal:controller.signal}))
|
||
.then((sonuclar) => {
|
||
if (iptal) return;
|
||
setKalemSonucu({query:normalizeTenderSearch(q),status:'loaded',matches:sonuclar.length ? new Map(sonuclar.map((s) => [s.ikn, s.eslesen])) : null});
|
||
})
|
||
.catch(error => { if (!iptal) setKalemSonucu({query:normalizeTenderSearch(q),status:'error',error:error.message,matches:null}); });
|
||
}, 350);
|
||
return () => { iptal = true; controller.abort(); clearTimeout(zaman); };
|
||
}, [searchQuery,kalemRetry]);
|
||
const [selectedCity, setSelectedCity] = useState('');
|
||
const [selectedRegion,setSelectedRegion]=useState('');
|
||
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
|
||
const [selectedKurumTipi, setSelectedKurumTipi] = useState('');
|
||
// Çoklu sektör seçimi. Tek seçimlik eski API (selectedSektor) uyumluluk
|
||
// için korunur: dizinin ilk elemanını okur, yazınca diziyi tek elemana indirir.
|
||
const [selectedSektorler, setSelectedSektorler] = useState([]);
|
||
const selectedSektor = selectedSektorler[0] || '';
|
||
const setSelectedSektor = (v) => setSelectedSektorler(v ? [v] : []);
|
||
const toggleSektor = (sek) =>
|
||
setSelectedSektorler((prev) => (prev.includes(sek) ? prev.filter((s) => s !== sek) : [...prev, sek]));
|
||
|
||
// CPV filtresi (bölüm kodu bazlı, çoklu seçim) — sektör deseniyle birebir aynı.
|
||
const [selectedCpvler, setSelectedCpvler] = useState([]);
|
||
const toggleCpv = (kod) =>
|
||
setSelectedCpvler((prev) => (prev.includes(kod) ? prev.filter((k) => k !== kod) : [...prev, kod]));
|
||
|
||
// Sektör/kurum listeleri sunucudan gelir; sabit liste yalnızca yedektir.
|
||
const [sektorListesi, setSektorListesi] = useState(SECTORS_LIST);
|
||
const [kurumTipiListesi, setKurumTipiListesi] = useState(INSTITUTION_TYPES_LIST);
|
||
// CPV bölüm listesi için statik bir yedek yok — yalnızca sunucudan gelir.
|
||
const [cpvBolumListesi, setCpvBolumListesi] = useState([]);
|
||
useEffect(() => {
|
||
fetch('/api/meta/filtreler')
|
||
.then((r) => r.json())
|
||
.then((d) => {
|
||
if (d?.sektorler?.length) setSektorListesi(d.sektorler);
|
||
if (d?.kurumTipleri?.length) setKurumTipiListesi(d.kurumTipleri);
|
||
if (d?.cpvBolumler?.length) setCpvBolumListesi(d.cpvBolumler);
|
||
})
|
||
.catch(() => {});
|
||
}, []);
|
||
const [onlyValidOnly, setOnlyValidOnly] = useState(true);
|
||
const [quickFilter, setQuickFilter] = useState('all');
|
||
|
||
const [savedFilters, setSavedFilters] = useState([]);
|
||
|
||
const [isLiveMode, setIsLiveMode] = useState(true);
|
||
const [isRefreshingLive, setIsRefreshingLive] = useState(false);
|
||
const [liveDataError, setLiveDataError] = useState(null);
|
||
const [liveDataSource, setLiveDataSource] = useState(null);
|
||
|
||
// --- Kimlik doğrulama (Google ile Giriş) ---
|
||
const [authUser, setAuthUser] = useState(() => getStoredUser());
|
||
const [authReady, setAuthReady] = useState(false);
|
||
const isAuthenticated = Boolean(authUser);
|
||
const isAdmin = Boolean(authUser?.isAdmin);
|
||
|
||
useEffect(() => {
|
||
restoreSession()
|
||
.then((u) => setAuthUser(u))
|
||
.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ü.
|
||
const [oncekiGiris, setOncekiGiris] = useState(null);
|
||
useEffect(() => {
|
||
if (!authUser) return;
|
||
try {
|
||
const anahtar = 'ihaleist_son_giris:' + authUser.email;
|
||
const onceki = Number(localStorage.getItem(anahtar)) || null;
|
||
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:' + authUser.email, String(onceki || 0));
|
||
localStorage.setItem(anahtar, String(Date.now()));
|
||
} catch { /* depolama kapalıysa kart bugünü gösterir */ }
|
||
}, [authUser?.email]);
|
||
|
||
const dataRevisions=useRef({});
|
||
// Giriş yapınca kullanıcı verisini SUNUCUDAN yükle (localStorage'ı ezer)
|
||
useEffect(() => {
|
||
notificationVersion.current++;notificationMutation.current=null;seenServerNotifications.current.clear();setNotificationError(null);setNotificationBusy(false);
|
||
aiRunRef.current=null;setAiAnalyzing(false);setAiJobStatus('');
|
||
dataRevisions.current={};setTeamError(null);setCompetitorError(null);setReminderError(null);
|
||
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([getUserDataSnapshot(), getTenderStates()])
|
||
.then(([snapshot, records]) => {
|
||
if (cancelled || getToken() !== loadSession) return;
|
||
const v=snapshot.veri;dataRevisions.current=snapshot.revisions;
|
||
setFavoriteKurumlar(v.favori_kurumlar || []);
|
||
setTeamMembers(v.ekip || []);
|
||
setSavedFilters(v.saved_filters || []);
|
||
setReminderSettings({...defaultReminder(), ...v.reminder});
|
||
setCompetitorWatchlist(v.rakip_izleme || []);
|
||
setUserProfile({...defaultProfile(authUser), ...v.firma_profil});
|
||
savedTenderStates.current = records;
|
||
setTenders(prev => applyTenderStates(prev.map(publicTender), records));
|
||
setVeriSenkron(true);
|
||
setSaveError(null);
|
||
})
|
||
.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]);
|
||
|
||
// --- Sunucu tarafı kullanıcı verisi ---
|
||
// localStorage yalnızca çevrimdışı önbellek; kaynak doğruluk sunucuda.
|
||
const [plan, setPlan] = useState(null);
|
||
const [veriSenkron, setVeriSenkron] = useState(false);
|
||
const savedTenderStates = useRef({});
|
||
const inFlight = useRef(new Set());
|
||
const [saveError, setSaveError] = useState(null);
|
||
const [savingCount, setSavingCount] = useState(0);
|
||
const mutateTender = async (tenderId, patch) => {
|
||
const t = tenders.find(t => t.id === tenderId);
|
||
if (!t || !veriSenkron || !authUser) { setSaveError('Hesap verileri yüklenmeden kayıt yapılamaz.'); return false; }
|
||
const key = tenderStateKey(t);
|
||
if (inFlight.current.has(key)) { setSaveError('Bu ihale için önceki kayıt sürüyor. Tamamlandıktan sonra tekrar deneyin.'); 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 || 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 && getToken() === session) {
|
||
savedTenderStates.current = records;
|
||
setTenders(prev => applyTenderStates(prev.map(publicTender), records));
|
||
}
|
||
} catch { /* retain visible error */ }
|
||
}
|
||
return false;
|
||
} finally { inFlight.current.delete(key); setSavingCount(n => n - 1); }
|
||
};
|
||
|
||
// Sunucu verisi yüklendiğinde her effect bir kez tetiklenir; bu ilk turda
|
||
// yazmak gereksiz (aynı veri) ve boş istemci state'iyle ezme riski taşır.
|
||
const ilkYazmaAtlanan = useRef(new Set());
|
||
// İlk tetiklenme (sunucu verisi yüklenirken) yazılmaz; sonrakiler yazılır.
|
||
const ilkYazmaDegil = (anahtar, deger) => {
|
||
if (ilkYazmaAtlanan.current.has(anahtar)) { sunucuyaYaz(anahtar, deger); return; }
|
||
ilkYazmaAtlanan.current.add(anahtar);
|
||
};
|
||
|
||
|
||
const sunucuyaYaz = (anahtar, deger) => {
|
||
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 = () => { const session=getToken(); return getPlan().then(p => {if(getToken()===session)setPlan(p);}).catch(() => {}); };
|
||
|
||
const handleLogin = (user) => setAuthUser(user);
|
||
const handleLogout = () => {
|
||
clearSession();
|
||
setAuthUser(null);
|
||
savedTenderStates.current = {};
|
||
setTenders([]);
|
||
setActiveTenderDetail(null);
|
||
};
|
||
|
||
// Canlı ihale verisini (ilan.gov.tr) çeker; kullanıcının kanban/takip durumunu korur.
|
||
const refreshLiveEKAPData = async ({ refresh = true } = {}) => {
|
||
setIsRefreshingLive(true);
|
||
setLiveDataError(null);
|
||
try {
|
||
const data = await fetchLiveTenders({ refresh });
|
||
setTenders(prev => applyTenderStates(mergeUserState(data.tenders, prev), savedTenderStates.current));
|
||
setLiveDataSource({ source: data.source, fetchedAt: data.fetchedAt });
|
||
if(data.historyWarning)setLiveDataError(data.historyWarning);
|
||
setLiveStats({
|
||
toplamIhale: data.totalCount ?? 0,
|
||
bugunEklenen: data.todayCount ?? 0,
|
||
arsiv: data.historyCount ?? 0,
|
||
});
|
||
} catch (err) {
|
||
setLiveDataError(err.message);
|
||
} finally {
|
||
setIsRefreshingLive(false);
|
||
}
|
||
};
|
||
|
||
// Açılışta canlı veriyi yükle (başarısız olursa mevcut/mock veri ekranda kalır)
|
||
useEffect(() => {
|
||
refreshLiveEKAPData({ refresh: false });
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
const [activeTenderDetail, setActiveTenderDetail] = useState(null);
|
||
const openSearchTender = (result) => {
|
||
const existing=tenders.find(t=>t.ikn===result.ikn);
|
||
const candidate=existing || searchTender(result);
|
||
const merged=applyTenderStates([candidate],savedTenderStates.current).find(t=>t.ikn===candidate.ikn) || candidate;
|
||
if(!existing) setTenders(previous=>previous.some(t=>t.ikn===merged.ikn) ? previous : [...previous,merged]);
|
||
setActiveTenderDetail(merged);
|
||
};
|
||
|
||
const [detailLoad, setDetailLoad] = useState(null);
|
||
const [detailRetry, setDetailRetry] = useState(0);
|
||
const retryTenderDetail = () => setDetailRetry(value => value + 1);
|
||
|
||
// Canlı ihalede detay açıldığında ilan metnini API'den tamamla
|
||
useEffect(() => {
|
||
const t = activeTenderDetail;
|
||
if (!t || !(t.id?.startsWith('ilan-') || t.id?.startsWith('ekap-')) || t.aciklama) { setDetailLoad(null); return; }
|
||
let cancelled = false;
|
||
const controller = new AbortController();
|
||
setDetailLoad({id:t.id,status:'loading'});
|
||
fetchTenderDetail(t.id, {signal:controller.signal})
|
||
.then(detail => {
|
||
if (cancelled) return;
|
||
const source = publicTender(detail);
|
||
source.sourceChanges = tenderSourceChanges(savedTenderStates.current[tenderStateKey(t)]?.snapshot, {...t,...source});
|
||
setDetailLoad({id:t.id,status:'loaded'});
|
||
setActiveTenderDetail(prev =>
|
||
prev && prev.id === t.id ? { ...prev, ...source } : prev
|
||
);
|
||
setTenders(prev =>
|
||
prev.map(x => (x.id === t.id ? { ...x, ...source } : x))
|
||
);
|
||
})
|
||
.catch(error => { if (!cancelled) setDetailLoad({id:t.id,status:'error',error:error.message}); });
|
||
return () => { cancelled = true; controller.abort(); };
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [activeTenderDetail?.id, detailRetry]);
|
||
const [isDetailedSearchOpen, setIsDetailedSearchOpen] = useState(false);
|
||
const [isCompanyScannerOpen, setIsCompanyScannerOpen] = useState(false);
|
||
const [isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen] = useState(false);
|
||
// Ürün araması: metni ihale dokümanlarının içinde arar (EKAP indeksi)
|
||
const [isUrunAramaOpen, setIsUrunAramaOpen] = useState(false);
|
||
// Sol menüden OKAS koduyla açılınca modal bu kodla başlar
|
||
const [urunAramaBaslangic, setUrunAramaBaslangic] = useState(null);
|
||
|
||
const [aiAnalysisResults, setAiAnalysisResults] = useState(null);
|
||
// Analiz modal dışından (ör. doküman panelindeki derin analiz düğmesi) da
|
||
// tetiklenebiliyor; modalın beklerken boş görünmemesi için ortak sinyal.
|
||
const [aiAnalyzing, setAiAnalyzing] = useState(false);
|
||
const aiRunRef=useRef(null);
|
||
const [aiJobStatus,setAiJobStatus]=useState('');
|
||
|
||
// Enterprise Module States
|
||
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (hesaba özel sunucu kaydı)
|
||
const [competitorWatchlist, setCompetitorWatchlist] = useState([]);
|
||
const competitorSaveLock=useRef(false);
|
||
const [competitorSaving,setCompetitorSaving]=useState(false);
|
||
const [competitorError,setCompetitorError]=useState(null);
|
||
const saveCompetitorWatchlist=async change=>{
|
||
if(!authUser||!veriSenkron){setCompetitorError('Hesap verileri yüklenmeden rakip listesi kaydedilemez.');return false;}
|
||
if(competitorSaveLock.current)return false;
|
||
const session=getToken();competitorSaveLock.current=true;setCompetitorSaving(true);setCompetitorError(null);
|
||
try {
|
||
const next=typeof change==='function'?change(competitorWatchlist):change;
|
||
const saved=await setUserData('rakip_izleme',next,dataRevisions.current.rakip_izleme ?? null);
|
||
if(getToken()!==session)return false;
|
||
dataRevisions.current.rakip_izleme=saved.revision;setCompetitorWatchlist(next);return true;
|
||
}catch(error){if(getToken()===session)setCompetitorError(error.message);return false;}
|
||
finally{competitorSaveLock.current=false;setCompetitorSaving(false);}
|
||
};
|
||
|
||
// --- Favori kurumlar (sunucu kaydı) ---
|
||
const [favoriteKurumlar, setFavoriteKurumlar] = useState([]);
|
||
const favoriteSaveLock=useRef(false);
|
||
const [favoriteSaving,setFavoriteSaving]=useState(false);
|
||
const toggleFavoriteKurum = async kurum => {
|
||
const ad=typeof kurum==='string' ? kurum.trim() : '';
|
||
if(!ad || favoriteSaveLock.current)return false;
|
||
if(!authUser||!veriSenkron){setSaveError('Hesap verileri yüklenmeden favoriler kaydedilemez.');return false;}
|
||
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ı) ---
|
||
const [teamMembers, setTeamMembers] = useState([]);
|
||
const teamSaveLock=useRef(false);
|
||
const [teamSaving,setTeamSaving]=useState(false);
|
||
const [teamError,setTeamError]=useState(null);
|
||
const persistTeam=async next=>{
|
||
if(!authUser||!veriSenkron){setTeamError('Hesap verileri yüklenmeden ekip kaydedilemez.');return false;}
|
||
if(teamSaveLock.current)return false;
|
||
const session=getToken();teamSaveLock.current=true;setTeamSaving(true);setTeamError(null);
|
||
try {
|
||
validateTeam(next);
|
||
const saved=await setUserData('ekip',next,dataRevisions.current.ekip ?? null);
|
||
if(getToken()!==session)return false;
|
||
dataRevisions.current.ekip=saved.revision;setTeamMembers(next);return true;
|
||
} catch(error){if(getToken()===session)setTeamError(error.message);return false;}
|
||
finally{teamSaveLock.current=false;setTeamSaving(false);}
|
||
};
|
||
const addTeamMember = member => persistTeam([...teamMembers,{...member,id:'tm-'+crypto.randomUUID()}]);
|
||
const removeTeamMember = id => persistTeam(teamMembers.filter(member=>member.id!==id));
|
||
|
||
// Belge kasası — kullanıcı kendi belgelerini ekler (hesaba özel sunucu kaydı)
|
||
const [vaultDocuments, setVaultDocuments] = useState([]);
|
||
|
||
const [reminderSettings, setReminderSettings] = useState(defaultReminder);
|
||
const reminderSaveLock=useRef(false),reminderCurrent=useRef(reminderSettings);
|
||
reminderCurrent.current=reminderSettings;
|
||
const [reminderSaving,setReminderSaving]=useState(false),[reminderError,setReminderError]=useState(null);
|
||
const saveReminderSettings=async change=>{
|
||
if(!authUser||!veriSenkron){setReminderError('Hesap verileri yüklenmeden bildirim ayarları kaydedilemez.');return false;}
|
||
if(reminderSaveLock.current)return false;
|
||
const session=getToken();reminderSaveLock.current=true;setReminderSaving(true);setReminderError(null);
|
||
try {
|
||
const next=validateReminderPreferences(typeof change==='function'?change(reminderCurrent.current):change);
|
||
const saved=await setUserData('reminder',next,dataRevisions.current.reminder ?? null);
|
||
if(getToken()!==session)return false;
|
||
dataRevisions.current.reminder=saved.revision;reminderCurrent.current=next;setReminderSettings(next);return true;
|
||
}catch(error){if(getToken()===session)setReminderError(error.message);return false;}
|
||
finally{reminderSaveLock.current=false;setReminderSaving(false);}
|
||
};
|
||
const isQuietNow=()=>quietNow(reminderCurrent.current);
|
||
|
||
const [webhooksConfig, setWebhooksConfig] = useState({
|
||
slackWebhookUrl: '',
|
||
teamsWebhookUrl: '',
|
||
events: {
|
||
newTenderMatch: true,
|
||
tenderWon: true,
|
||
bidDeadlineAlert: true,
|
||
competitorActivity: true
|
||
}
|
||
});
|
||
|
||
const [apiKeys, setApiKeys] = useState([]);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
||
// ---------------------------------------------------------------------------
|
||
const AI_MODEL_CATALOG = [
|
||
{ id: 'glmflash', label: 'GLM 5.3 Flash', provider: 'Z.ai', or: 'z-ai/glm-5.3-flash', model: 'glm-5.3-flash', renk: 'blue' },
|
||
{ id: 'gemini', label: 'Gemini Flash Latest', provider: 'Google', or: '~google/gemini-flash-latest', model: 'gemini-flash-latest', renk: 'amber' },
|
||
{ id: 'deepseek', label: 'DeepSeek V4 Flash Latest', provider: 'DeepSeek', or: '~deepseek/deepseek-v4-flash-latest', model: 'deepseek-chat', renk: 'indigo' },
|
||
{ id: 'grok', label: 'Grok Latest', provider: 'xAI', or: '~x-ai/grok-latest', model: 'grok-4.6', renk: 'slate' },
|
||
{ id: 'gpt', label: 'GPT Chat Latest', provider: 'OpenAI', or: 'openai/gpt-chat-latest', model: 'gpt-5.6-sol', renk: 'emerald' },
|
||
{ id: 'claude', label: 'Claude Opus Latest', provider: 'Anthropic', or: '~anthropic/claude-opus-latest', model: 'claude-opus-5', renk: 'orange' }
|
||
];
|
||
|
||
// Varsayılan fallback zinciri: GLM 5.3 Flash (birincil) → Gemini → DeepSeek
|
||
// (glm-latest kaldırıldı: birincil ile aynı modele çözünüyordu, gerçek yedek değildi)
|
||
const VARSAYILAN_ZINCIR = ['glmflash', 'gemini', 'deepseek'];
|
||
const ZINCIR_SURUM = 3; // artırıldığında kayıtlı zincir yeni düzene taşınır
|
||
|
||
const [aiModelConfig, setAiModelConfig] = useState(() => {
|
||
const saved = localStorage.getItem('ihaleist_ai_model_config');
|
||
if (saved) {
|
||
try {
|
||
const cfg = JSON.parse(saved);
|
||
// Geçiş: kayıtlı zinciri yeni düzene taşı (glm çıkar, glmflash → gemini başa)
|
||
if ((cfg.surum || 1) < ZINCIR_SURUM) {
|
||
const chain = Array.isArray(cfg.chain) ? cfg.chain : [];
|
||
const kalan = chain.filter((id) => !['glmflash', 'gemini', 'glm'].includes(id));
|
||
return { ...cfg, surum: ZINCIR_SURUM, chain: ['glmflash', 'gemini', ...kalan] };
|
||
}
|
||
// Katalogdan çıkarılmış modelleri kayıtlı zincirden ayıkla
|
||
if (Array.isArray(cfg.chain)) {
|
||
cfg.chain = cfg.chain.filter((id) => AI_MODEL_CATALOG.some((m) => m.id === id));
|
||
if (!cfg.chain.length) cfg.chain = VARSAYILAN_ZINCIR;
|
||
}
|
||
return cfg;
|
||
} catch { /* yoksay */ }
|
||
}
|
||
return { chain: VARSAYILAN_ZINCIR, surum: ZINCIR_SURUM };
|
||
});
|
||
|
||
useEffect(() => {
|
||
yerelKaydet('ihaleist_ai_model_config', aiModelConfig);
|
||
}, [aiModelConfig]);
|
||
|
||
const getModelById = (id) => AI_MODEL_CATALOG.find(m => m.id === id);
|
||
const activeAiModel = getModelById(aiModelConfig.chain[0]) || AI_MODEL_CATALOG[0];
|
||
|
||
// Gerçek AI analizi — kalıcı sunucu işi üzerinden, admin panelde
|
||
// 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, extraction, reviewedSource } = {}) => {
|
||
const session = getToken();
|
||
if(aiRunRef.current?.session===session)throw new Error('Bir analiz zaten sürüyor. Kayıtlı işlerden durumunu kontrol edin.');
|
||
const run={session};aiRunRef.current=run;
|
||
setAiAnalyzing(true);
|
||
setAiAnalysisResults(null);
|
||
try {
|
||
const { analyzeSpecText } = await import('../services/ai');
|
||
const data = await analyzeSpecText({ text, fileName, tender, chain: aiModelConfig.chain, mode, extraction, reviewedSource },{onProgress:job=>{if(getToken()===session && aiRunRef.current===run)setAiJobStatus(`${job.id}:${job.state}`);}});
|
||
const {analysisView}=await import('../services/ai');
|
||
const result=analysisView(data,{tender,mode,fileName});
|
||
if(getToken() !== session || aiRunRef.current!==run) throw new Error('Oturum değişti; analiz sonucu bu hesaba aktarılmadı.');
|
||
setAiAnalysisResults(result);
|
||
return result;
|
||
} finally {
|
||
if(aiRunRef.current===run){aiRunRef.current=null;setAiAnalyzing(false);}
|
||
}
|
||
};
|
||
|
||
const openSavedAnalysis = async job => {
|
||
const session=getToken();
|
||
if(job?.operation!=='analiz'||job.state!=='completed')throw new Error('Tamamlanmış analiz seçin.');
|
||
const {analysisView}=await import('../services/ai');
|
||
const result=analysisView(job.result,job.input);
|
||
if(getToken()!==session)throw new Error('Oturum değişti.');
|
||
setAiAnalysisResults(result);
|
||
return result;
|
||
};
|
||
|
||
const [userProfile, setUserProfile] = useState(() => defaultProfile(getStoredUser()));
|
||
|
||
// Anlamsal bonus: profil metni sunucuda gömülür, havuzla kosinüs alınır,
|
||
// z-skor → 0..20. Profil terimi yoksa ya da oturum yoksa istenmez. Havuz
|
||
// 10 dk'da bir yenilendiği için ihale sayısı değişince tazelenir.
|
||
const [gommeBonus, setGommeBonus] = useState(null);
|
||
const [gommeDurum, setGommeDurum] = useState('kapali'); // kapali | bekliyor | hazir | hata
|
||
const profilMetin = useMemo(() => profilMetni(userProfile), [userProfile.scrapedCapabilities]);
|
||
useEffect(() => {
|
||
if (!profilTerimleri(userProfile).length || !getToken() || !tenders.length) { setGommeBonus(null); return; }
|
||
let iptal = false;
|
||
setGommeDurum('bekliyor');
|
||
const z = setTimeout(() => {
|
||
fetch('/api/uyum/gomme', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
|
||
body: JSON.stringify({ profilMetni: profilMetin }),
|
||
})
|
||
.then((r) => r.json())
|
||
.then((d) => {
|
||
if (iptal) return;
|
||
if (d.success && d.hazir) { setGommeBonus(d.bonus || {}); setGommeDurum('hazir'); }
|
||
else { setGommeBonus(null); setGommeDurum(d.success ? 'kapali' : 'hata'); }
|
||
})
|
||
.catch(() => { if (!iptal) { setGommeBonus(null); setGommeDurum('hata'); } });
|
||
}, 800);
|
||
return () => { iptal = true; clearTimeout(z); };
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [profilMetin, tenders.length]);
|
||
|
||
// Uyum skoru: sözlüksel (anında, açıklanabilir) + anlamsal bonus (sunucu).
|
||
const tendersSkorlu = useMemo(
|
||
() => havuzuPuanla(tenders, userProfile, gommeBonus),
|
||
[tenders, userProfile.scrapedCapabilities, gommeBonus],
|
||
);
|
||
|
||
const scanCompanyWebsite = scanCompanyProposal;
|
||
const profileSaveLock=useRef(false);
|
||
const updateCompanyCapabilities = async (capabilities,{website=userProfile.website,threshold=userProfile.uyumEsigi}={}) => {
|
||
if(!authUser||!veriSenkron)throw new Error('Hesap verileri yüklenmeden profil kaydedilemez.');
|
||
if(profileSaveLock.current)throw new Error('Önceki profil kaydı sürüyor.');
|
||
const session=getToken();
|
||
profileSaveLock.current=true;
|
||
try {
|
||
const profile={company:userProfile.company,website,scrapedCapabilities:capabilities,uyumEsigi:Math.max(0,Math.min(100,Number(threshold)||0))};
|
||
const saved=await setUserData('firma_profil',profile,dataRevisions.current.firma_profil ?? null);
|
||
if(getToken()!==session)throw new Error('Oturum değişti; profil bu hesaba aktarılmadı.');
|
||
dataRevisions.current.firma_profil=saved.revision;
|
||
setUserProfile(previous=>({...previous,...profile}));
|
||
return profile;
|
||
}finally{profileSaveLock.current=false;}
|
||
};
|
||
|
||
const [notifications, setNotifications] = useState([]);
|
||
const [notificationError,setNotificationError]=useState(null);
|
||
const [notificationBusy,setNotificationBusy]=useState(false);
|
||
const notificationVersion=useRef(0),notificationMutation=useRef(null),seenServerNotifications=useRef(new Set());
|
||
|
||
// Gerçek bildirim: listeye ekler; sessiz saat dışında ve push açıksa tarayıcı
|
||
// bildirimi (Notification API) da gösterir.
|
||
const notify = (title, text) => {
|
||
setNotifications(prev=>addLocalNotification(prev,title,text));
|
||
pushNotification(title,text);
|
||
};
|
||
const pushNotification=(title,text)=>{
|
||
if (isQuietNow() || !reminderCurrent.current.pushEnabled) return;
|
||
try {
|
||
if ('Notification' in window && Notification.permission === 'granted') {
|
||
new Notification(title, { body: text, icon: '/favicon.ico' });
|
||
}
|
||
} catch { /* yoksay */ }
|
||
};
|
||
const requestPushPermission = async () => {
|
||
if (!('Notification' in window)) return 'unsupported';
|
||
if (Notification.permission !== 'default') return Notification.permission;
|
||
return await Notification.requestPermission();
|
||
};
|
||
const refreshNotifications=async()=>{
|
||
const session=getToken(),version=++notificationVersion.current;
|
||
setNotificationError(null);
|
||
try {
|
||
const rows=await bildirimleriGetir();
|
||
if(getToken()!==session||version!==notificationVersion.current)return;
|
||
const checked=mergeServerNotifications([],rows);
|
||
const fresh=checked.filter(row=>!row.read&&!seenServerNotifications.current.has(row.id));
|
||
checked.forEach(row=>seenServerNotifications.current.add(row.id));
|
||
fresh.slice(0,5).forEach(row=>pushNotification(row.title,row.text));
|
||
setNotifications(current=>mergeServerNotifications(current,rows));
|
||
}catch(error){if(getToken()===session&&version===notificationVersion.current)setNotificationError(error.message);}
|
||
};
|
||
const markNotificationsRead=async()=>{
|
||
if(notificationMutation.current)return;
|
||
const operation={session:getToken()};notificationMutation.current=operation;
|
||
notificationVersion.current++;setNotificationBusy(true);setNotificationError(null);
|
||
try {
|
||
const ids=await acknowledgeNotifications(notifications,bildirimOkundu);
|
||
if(getToken()!==operation.session||notificationMutation.current!==operation)return;
|
||
notificationVersion.current++;
|
||
setNotifications(current=>current.map(row=>ids.has(row.id)?{...row,read:true}:row));
|
||
}catch(error){if(getToken()===operation.session&¬ificationMutation.current===operation)setNotificationError(error.message);}
|
||
finally {if(notificationMutation.current===operation){notificationMutation.current=null;setNotificationBusy(false);}}
|
||
};
|
||
|
||
// Persist state to localstorage
|
||
// Havuz on binlerce kayda çıkabiliyor; tamamı localStorage kotasını (≈5 MB)
|
||
// aşar ve yakalanmazsa uygulama beyaz ekranla çöker. Bu yüzden yalnızca ilk
|
||
// 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).map(publicTender)));
|
||
} catch {
|
||
// Kota dolduysa önbelleği bırak — veri zaten sunucudan geliyor.
|
||
try { localStorage.removeItem(TENDER_CACHE_KEY); } catch { /* yok say */ }
|
||
}
|
||
}, [tenders]);
|
||
|
||
|
||
// (Sahte "online kullanıcı" rastgele sayacı kaldırıldı — canlı istatistikler
|
||
// gerçek /api/stats verisinden gelir.)
|
||
|
||
const toggleTrackTender = (tenderId) => {
|
||
const t = tenders.find(t => t.id === tenderId);
|
||
return mutateTender(tenderId, {takipte: !t?.takipte});
|
||
};
|
||
|
||
// Takipteki İKN'ler sunucuya yazılır: zeyilname/düzeltme ilanı ve sonuç
|
||
// bildirimi sunucudaki zamanlayıcıda üretiliyor, tarayıcı kapalıyken de.
|
||
const takipIknler = useMemo(
|
||
() => tenders.filter((t) => t.takipte || t.gorev).map((t) => t.ikn).filter(Boolean).sort(),
|
||
[tenders],
|
||
);
|
||
const takipImza = takipIknler.join('|');
|
||
useEffect(() => {
|
||
if (veriSenkron) ilkYazmaDegil('takip', takipIknler);
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [takipImza, veriSenkron]);
|
||
|
||
/**
|
||
* İhaleden hazırlık görevi: teslim tarihi ihale saatidir, panoya "Teklif
|
||
* Hazırlanıyor" kolonunda düşer, takibe alınır — sonuç yayımlanınca bildirim
|
||
* gelir. (Fikir: MOBIT-DURT "ilandan göreve, görevden sonuca".)
|
||
*/
|
||
const gorevAc = async (tenderId, { kontrolListesi = [] } = {}) => {
|
||
const t = tenders.find(t => t.id === tenderId);
|
||
if (!t || t.gorev) return;
|
||
const ok = await mutateTender(tenderId, {
|
||
takipte: true,
|
||
kanbanStatus: t.kanbanStatus && t.kanbanStatus !== 'incelenecek' ? t.kanbanStatus : 'teklif_hazirlaniyor',
|
||
gorev: { acildi: new Date().toISOString(), teslim: t.tarih ? `${t.tarih} ${t.saat || ''}`.trim() : null,
|
||
kontrolListesi: kontrolListesi.map(k => ({ad:typeof k === 'string' ? k : k.document,tamam:false})) },
|
||
});
|
||
if (ok) notify('Hazırlık görevi kaydedildi', 'Görev sunucuda saklandı; Kanban panosundan devam edebilirsiniz.');
|
||
};
|
||
const gorevKontrol = (tenderId, idx) => {
|
||
const t = tenders.find(t => t.id === tenderId);
|
||
if (!t?.gorev) return;
|
||
return mutateTender(tenderId, {gorev:{...t.gorev,kontrolListesi:t.gorev.kontrolListesi.map((k,i) => i === idx ? {...k,tamam:!k.tamam} : k)}});
|
||
};
|
||
|
||
// Sunucu bildirimleri (zeyilname / sonuç): 5 dk'da bir çek, yenileri göster
|
||
useEffect(() => {
|
||
if (!veriSenkron) return;
|
||
void refreshNotifications();
|
||
const timer=setInterval(refreshNotifications,5*60*1000);
|
||
return()=>{notificationVersion.current++;clearInterval(timer);};
|
||
}, [veriSenkron,authUser?.email]);
|
||
|
||
// Şirket belgesi süre uyarısı: süresi dolmuş / 30 gün içinde dolacak
|
||
// belgeler için günde bir kez bildirim (kimse teklif hazırlanana kadar
|
||
// 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(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 || []) {
|
||
if (!b.son_kullanma) continue;
|
||
const kalan = Math.ceil((new Date(b.son_kullanma).getTime() - simdi) / 86400000);
|
||
if (kalan < 0) dolan.push(b.ad); else if (kalan <= 30) yakin.push(`${b.ad} (${kalan} gün)`);
|
||
}
|
||
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(uyariAnahtari, bugun); } catch { /* yoksay */ }
|
||
}).catch(() => {});
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [veriSenkron]);
|
||
|
||
const updateKanbanStatus = (tenderId, newStatus, assignedUser = null) => {
|
||
const t = tenders.find(t => t.id === tenderId);
|
||
return mutateTender(tenderId, {kanbanStatus:newStatus,atananPersonel:assignedUser ?? t?.atananPersonel ?? null,takipte:true});
|
||
};
|
||
|
||
const filterSaveLock=useRef(false);
|
||
const persistSavedFilters=async next=>{
|
||
if(!authUser||!veriSenkron){setSaveError('Hesap verileri yüklenmeden filtre kaydedilemez.');return false;}
|
||
if(filterSaveLock.current){setSaveError('Önceki filtre kaydı sürüyor.');return false;}
|
||
const session=getToken();filterSaveLock.current=true;setSavingCount(n=>n+1);setSaveError(null);
|
||
try{
|
||
const saved=await setUserData('saved_filters',next,dataRevisions.current.saved_filters ?? null);
|
||
if(getToken()!==session)return false;
|
||
dataRevisions.current.saved_filters=saved.revision;
|
||
setSavedFilters(next);return true;
|
||
}catch(error){if(getToken()===session)setSaveError(error.message);return false;}
|
||
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 applySavedFilter = filter => {
|
||
try {
|
||
const criteria=savedFilterCriteria(filter);
|
||
setDetailedQueries({institution:'',registration:''});
|
||
setSearchQuery('');setSelectedCpvler([]);setQuickFilter('all');
|
||
setSelectedCity(criteria.city);setSelectedRegion(criteria.region);
|
||
setSelectedAlimTuru(criteria.procedure);setSelectedKurumTipi(criteria.institution);
|
||
setSelectedSektorler(criteria.sector?[criteria.sector]:[]);setActiveTab('list');setSaveError(null);
|
||
return true;
|
||
}catch(error){setSaveError(error.message);return false;}
|
||
};
|
||
|
||
const clearAllFilters = () => {
|
||
setDetailedQueries({institution:'',registration:''});
|
||
setSelectedRegion('');
|
||
setSearchQuery('');
|
||
setSelectedCity('');
|
||
setSelectedAlimTuru('');
|
||
setSelectedKurumTipi('');
|
||
setSelectedSektorler([]);
|
||
setSelectedCpvler([]);
|
||
setQuickFilter('all');
|
||
};
|
||
|
||
return (
|
||
<AppContext.Provider value={{
|
||
activeTab,
|
||
setActiveTab,
|
||
tenders: tendersSkorlu,
|
||
uyumEsigi: userProfile.uyumEsigi,
|
||
gommeDurum,
|
||
setTenders,
|
||
isLiveMode,
|
||
setIsLiveMode,
|
||
isRefreshingLive,
|
||
refreshLiveEKAPData,
|
||
liveDataError,
|
||
liveDataSource,
|
||
authUser,
|
||
authReady,
|
||
isAuthenticated,
|
||
isAdmin,
|
||
handleLogin,
|
||
handleLogout,
|
||
liveStats,
|
||
searchQuery,
|
||
detailedQueries,setDetailedQueries,
|
||
kalemEslesen, itemSearchState, retryItemSearch,
|
||
setSearchQuery,
|
||
selectedCity, selectedRegion,setSelectedRegion,applySavedFilter,
|
||
setSelectedCity,
|
||
selectedAlimTuru,
|
||
setSelectedAlimTuru,
|
||
selectedKurumTipi,
|
||
setSelectedKurumTipi,
|
||
selectedSektor,
|
||
setSelectedSektor,
|
||
selectedSektorler,
|
||
setSelectedSektorler,
|
||
toggleSektor,
|
||
sektorListesi,
|
||
kurumTipiListesi,
|
||
selectedCpvler,
|
||
setSelectedCpvler,
|
||
toggleCpv,
|
||
cpvBolumListesi,
|
||
onlyValidOnly,
|
||
oncekiGiris,
|
||
setOnlyValidOnly,
|
||
quickFilter,
|
||
setQuickFilter,
|
||
savedFilters,
|
||
addSavedFilter,
|
||
deleteSavedFilter,
|
||
activeTenderDetail,
|
||
openSearchTender,
|
||
detailLoad,
|
||
retryTenderDetail,
|
||
setActiveTenderDetail,
|
||
isDetailedSearchOpen,
|
||
setIsDetailedSearchOpen,
|
||
isCompanyScannerOpen,
|
||
setIsCompanyScannerOpen,
|
||
scanCompanyWebsite,
|
||
updateCompanyCapabilities,
|
||
isAiSpecAnalyzerOpen,
|
||
isUrunAramaOpen,
|
||
setIsUrunAramaOpen,
|
||
urunAramaBaslangic,
|
||
setUrunAramaBaslangic,
|
||
setIsAiSpecAnalyzerOpen,
|
||
analyzeSpecPdfWithGemini,
|
||
openSavedAnalysis,
|
||
aiAnalysisResults,
|
||
aiAnalyzing,
|
||
aiJobStatus,
|
||
competitorWatchlist,
|
||
saveCompetitorWatchlist,competitorSaving,competitorError,
|
||
vaultDocuments,
|
||
setVaultDocuments,
|
||
reminderSettings,
|
||
saveReminderSettings,reminderSaving,reminderError,
|
||
webhooksConfig,
|
||
setWebhooksConfig,
|
||
apiKeys,
|
||
setApiKeys,
|
||
AI_MODEL_CATALOG,
|
||
aiModelConfig,
|
||
setAiModelConfig,
|
||
getModelById,
|
||
activeAiModel,
|
||
userProfile,
|
||
notifications,
|
||
notify,
|
||
requestPushPermission,
|
||
markNotificationsRead,
|
||
refreshNotifications,notificationError,notificationBusy,
|
||
isQuietNow,
|
||
favoriteKurumlar,favoriteSaving,
|
||
toggleFavoriteKurum,
|
||
teamMembers,teamSaving,teamError,
|
||
addTeamMember,
|
||
removeTeamMember,
|
||
toggleTrackTender,
|
||
updateKanbanStatus,
|
||
gorevAc,
|
||
gorevKontrol,
|
||
clearAllFilters,
|
||
plan,
|
||
planYenile,
|
||
kotaUyarisi,
|
||
setKotaUyarisi,
|
||
veriSenkron, saveError, savingCount, mutateTender
|
||
}}>
|
||
{children}
|
||
</AppContext.Provider>
|
||
);
|
||
};
|
||
|
||
export const useApp = () => useContext(AppContext);
|