diff --git a/server/ai-teklif.mjs b/server/ai-teklif.mjs index b2ff5e1..291e68e 100644 --- a/server/ai-teklif.mjs +++ b/server/ai-teklif.mjs @@ -71,7 +71,7 @@ export async function belgeyeSor({ ikn, soru, text, tender, chain }) { const out = await analyzeSpec({ text: `PARÇALAR:\n\n${baglam}\n\nSORU: ${q}\n\nYalnızca {"cevap":"..."} JSON'u döndür.`, tender, chain, systemOverride: sistem }); const cevap = String(out?.result?.cevap ?? '').trim() || 'Yanıt üretilemedi.'; const kullanilan = new Set([...cevap.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]))); - return { cevap, kaynaklar: (kullanilan.size ? kaynaklar.filter((k) => kullanilan.has(k.n)) : kaynaklar.slice(0, 3)).map((k) => ({ n: k.n, alinti: k.icerik.slice(0, 1400) })), kaynak, model: out.model?.label }; + return { cevap, kaynaklar: (kullanilan.size ? kaynaklar.filter((k) => kullanilan.has(k.n)) : kaynaklar.slice(0, 3)).map((k) => ({ n: k.n, alinti: k.icerik.slice(0, 1400) })), kaynak }; } function gereklilikMetni(tender, gereklilikler, metin) { @@ -79,8 +79,17 @@ function gereklilikMetni(tender, gereklilikler, metin) { for (const x of Array.isArray(gereklilikler) ? gereklilikler : []) if (x) g.push(typeof x === 'string' ? x : x.belge || x.item || x.ad || JSON.stringify(x)); for (const y of Array.isArray(tender?.yeterlikKriterleri) ? tender.yeterlikKriterleri : []) if (y) g.push(String(y)); for (const i of Array.isArray(tender?.items) ? tender.items.slice(0, 30) : []) g.push(`Kalem: ${i.ad} ${i.miktar ?? ''} ${i.birim ?? ''}`.trim()); - if (tender?.ikn && kalemStore.available()) for (const k of (kalemStore.kalemOku(tender.ikn) || []).slice(0, 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim()); - if (metin) for (const s of ['teminat', 'teslim süresi', 'geçerlilik', 'ceza', 'numune', 'belge']) { const p = getir(parcala(metin), s, 1)[0]; if (p) g.push(`${s}: ${p.icerik.slice(0, 300).replace(/\s+/g, ' ')}`); } + if (tender?.ikn && kalemStore.available()) for (const k of ((kalemStore.kalemOku(tender.ikn) || {}).kalemler || []).slice(0, 30)) g.push(`Kalem: ${k.ad} ${k.miktar ?? ''} ${k.birim ?? ''}`.trim()); + if (metin) { + // Önce anahtar sözcüğü geçen SATIR (madde) alınır; bütün metni yapıştırmak okunmaz oluyordu. + const satirlar = String(metin).split(/\r?\n+/).map((x) => x.trim()).filter((x) => x.length > 15); + for (const s of ['teminat', 'teslim süresi', 'geçerlilik', 'ceza', 'numune', 'belge']) { + const satir = satirlar.find((x) => x.toLocaleLowerCase('tr-TR').includes(s)); + if (satir) { g.push(`${s}: ${satir.slice(0, 300).replace(/\s+/g, ' ')}`); continue; } + const p = getir(parcala(metin), s, 1)[0]; + if (p) g.push(`${s}: ${p.icerik.slice(0, 300).replace(/\s+/g, ' ')}`); + } + } return [...new Set(g)].slice(0, 60); } @@ -100,7 +109,7 @@ export async function teklifDenetle({ ikn, taslak, gereklilikler, tender, chain, }); const ok = maddeler.filter((m) => m.durum === 'karsilaniyor').length; const yarim = maddeler.filter((m) => m.durum === 'belirsiz').length; - return { ozet: String(out?.result?.ozet ?? '').slice(0, 800), maddeler, uyum_yuzde: Math.round(((ok + yarim * 0.5) / maddeler.length) * 100), eksikler: maddeler.filter((m) => m.durum === 'eksik').map((m) => m.gereklilik), model: out.model?.label }; + return { ozet: String(out?.result?.ozet ?? '').slice(0, 800), maddeler, uyum_yuzde: Math.round(((ok + yarim * 0.5) / maddeler.length) * 100), eksikler: maddeler.filter((m) => m.durum === 'eksik').map((m) => m.gereklilik) }; } const BOLUMLER = { @@ -127,5 +136,5 @@ export async function teklifTaslak({ ikn, tur, tender, firma, belgeler, fiyat, g .map((b) => ({ baslik: String(b?.baslik ?? '').slice(0, 200), icerik: String(b?.icerik ?? '').slice(0, 12000) })) .filter((b) => b.baslik && b.icerik); if (!bolumler.length) throw new Error('Model bölüm üretmedi; tekrar deneyin.'); - return { bolumler, model: out.model?.label }; + return { bolumler }; } diff --git a/server/ai.mjs b/server/ai.mjs index 65c3e1e..db194d6 100644 --- a/server/ai.mjs +++ b/server/ai.mjs @@ -276,7 +276,39 @@ function extractJson(raw) { const start = s.indexOf('{'); const end = s.lastIndexOf('}'); if (start === -1 || end === -1) throw new Error('Yanıtta JSON bulunamadı'); - return JSON.parse(s.slice(start, end + 1)); + const govde = s.slice(start, end + 1); + try { return JSON.parse(govde); } catch (e) { + const onarilmis = kesikJsonOnar(s.slice(start)); + if (onarilmis) return onarilmis; + throw e; + } +} + +/** + * Model çıktısı max_tokens'ta kesildiğinde JSON yarım kalır. Sondan geriye + * doğru güvenli kesme noktaları denenir; açık kalan dizi/nesneler kapatılır. + */ +function kesikJsonOnar(s) { + for (let cut = s.length; cut > 50; ) { + cut = Math.max(s.lastIndexOf('}', cut - 1), s.lastIndexOf(']', cut - 1), s.lastIndexOf('"', cut - 1)); + if (cut <= 0) return null; + let parca = s.slice(0, cut + 1).replace(/,\s*$/, ''); + // yığın: string dışındaki açık parantezler + const yigin = []; let strIcinde = false, kacis = false; + for (const ch of parca) { + if (strIcinde) { if (kacis) kacis = false; else if (ch === '\\') kacis = true; else if (ch === '"') strIcinde = false; continue; } + if (ch === '"') strIcinde = true; + else if (ch === '{' || ch === '[') yigin.push(ch); + else if (ch === '}' || ch === ']') yigin.pop(); + } + if (strIcinde) { cut -= 1; continue; } + parca = parca.replace(/,\s*$/, ''); + // "anahtar": ile biten yarım çift -> anahtarı at + parca = parca.replace(/,?\s*"[^"]*"\s*:\s*$/, ''); + while (yigin.length) parca += yigin.pop() === '{' ? '}' : ']'; + try { return JSON.parse(parca); } catch { cut -= 1; } + } + return null; } /** Zinciri sırayla dener; ilk başarılı sonucu döndürür. */ @@ -288,7 +320,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid // kesilirse JSON parse edilemez ve zincir boşuna tüketilir. const userPrompt = buildUserPrompt({ text, fileName, tender, limit: derin ? 90000 : 60000 }); const sistem = systemOverride || (derin ? DERIN_SYSTEM_PROMPT : SYSTEM_PROMPT); - const maxTokens = derin ? 8000 : 4000; + const maxTokens = derin ? 12000 : 4000; const timeoutMs = derin ? 150000 : 90000; const attempts = []; diff --git a/src/components/Header.jsx b/src/components/Header.jsx index be13e5f..e042e72 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -54,7 +54,7 @@ export const Header = () => { {/* Search Bar */} -
+
{ {/* Right Action Icons */} -
+
{/* Real Live EKAP Mode Badge */}
) : null; - return ( + return createPortal( <>
)} - + , + document.body, ); }; diff --git a/src/components/TenderCard.jsx b/src/components/TenderCard.jsx index 5e850be..d25aea9 100644 --- a/src/components/TenderCard.jsx +++ b/src/components/TenderCard.jsx @@ -137,7 +137,7 @@ export const TenderCard = ({ tender }) => { {tender.sektor} - {tender.cpvLabel && tender.cpvLabel !== 'Genel' && ( + {tender.cpvLabel && tender.cpvLabel !== 'Genel' && tender.cpvLabel !== tender.sektor && ( {tender.cpvLabel} diff --git a/src/components/TenderDetailModal.jsx b/src/components/TenderDetailModal.jsx index 5a3b628..95a6d24 100644 --- a/src/components/TenderDetailModal.jsx +++ b/src/components/TenderDetailModal.jsx @@ -10,12 +10,10 @@ import { import confetti from 'canvas-confetti'; export const TenderDetailModal = () => { - const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults } = useApp(); + const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults, tenders } = useApp(); const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders' const [noteText, setNoteText] = useState(''); - const [savedNotes, setSavedNotes] = useState([ - { id: 1, author: 'Hikmet Tanrıverdi', text: 'Kayseri deposuna doğrudan sevkiyat yapılacak. Birim fiyat 145 TL üstü olmalı.', date: 'Bugün 10:15' } - ]); + const [savedNotes, setSavedNotes] = useState([]); // Kur Farkı Calculator State const [bidAmount, setBidAmount] = useState(15000); @@ -27,7 +25,8 @@ export const TenderDetailModal = () => { const [isDraggingStamp, setIsDraggingStamp] = useState(false); if (!activeTenderDetail) return null; - const tender = activeTenderDetail; + // Havuzdaki güncel kayıt: takip/görev değişince düğme etiketleri de değişsin. + const tender = (activeTenderDetail && tenders.find((t) => t.id === activeTenderDetail.id)) || activeTenderDetail; const handleAddNote = (e) => { e.preventDefault(); @@ -176,7 +175,7 @@ END:VCALENDAR`; {[ { id: 'details', label: 'İhale Detayları', icon: FileText }, { id: 'pdf_stamp', label: 'Birim Fiyat Cetveli (PDF & Kaşe)', icon: Stamp, badge: 'İnteraktif' }, - { id: 'ai_assistant', label: 'Yapay Zeka Asistanı', icon: Sparkles, badge: '%94 Uyum' }, + { id: 'ai_assistant', label: 'Yapay Zeka Asistanı', icon: Sparkles, badge: tender?.aiMatchScore != null ? `%${tender.aiMatchScore} Uyum` : undefined }, { id: 'checklist', label: 'Hazırlık Checklist', icon: CheckSquare }, { id: 'teklif_asistani', label: 'Teklif Asistanı', icon: PenLine, badge: 'AI' }, ].map(tab => { diff --git a/src/components/tools/CompetitorRadar.jsx b/src/components/tools/CompetitorRadar.jsx index 7b905df..8ee4217 100644 --- a/src/components/tools/CompetitorRadar.jsx +++ b/src/components/tools/CompetitorRadar.jsx @@ -66,7 +66,7 @@ export const CompetitorRadar = () => { Proaktif Takip
-

İzlemeye aldığınız rakip firmalar ihaleye teklif verdiğinde veya kazandığında anında SMS ve Push bildirimi alın.

+

İzlemeye aldığınız rakip firmalar ihaleye teklif verdiğinde veya kazandığında uygulama içi, e-posta ve Telegram bildirimi alın.

diff --git a/src/components/tools/ContractSearch.jsx b/src/components/tools/ContractSearch.jsx index 2ff3c67..7c5bfbd 100644 --- a/src/components/tools/ContractSearch.jsx +++ b/src/components/tools/ContractSearch.jsx @@ -75,10 +75,10 @@ export const ContractSearch = () => {
Pazar Payı (hacim) - %{comp.share} + %{Number(comp.share).toLocaleString('tr-TR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}
-
+
diff --git a/src/components/tools/DailyBulletin.jsx b/src/components/tools/DailyBulletin.jsx index 19e9108..d2acfca 100644 --- a/src/components/tools/DailyBulletin.jsx +++ b/src/components/tools/DailyBulletin.jsx @@ -5,8 +5,14 @@ import { Newspaper, Calendar, Building2, Package, ArrowRight } from 'lucide-reac export const DailyBulletin = () => { const { tenders, setActiveTenderDetail } = useApp(); - const todayStr = '10.08.2026'; - const todayTenders = tenders.filter(t => t.tarih === todayStr || (t.eklenmeZamani || '').includes('az') || (t.eklenmeZamani || '').includes('önce')); + // Bugün: yayımlanma (ilan) tarihi bugün olanlar; ilan tarihi yoksa havuza az önce eklenenler. + const bugun = new Date(); + const todayStr = `${String(bugun.getDate()).padStart(2, '0')}.${String(bugun.getMonth() + 1).padStart(2, '0')}.${bugun.getFullYear()}`; + const isoBugun = `${bugun.getFullYear()}-${String(bugun.getMonth() + 1).padStart(2, '0')}-${String(bugun.getDate()).padStart(2, '0')}`; + const bugunMu = (v) => !!v && (String(v).startsWith(isoBugun) || String(v).slice(0, 10) === todayStr); + const todayTenders = tenders.filter(t => + bugunMu(t.ilanTarihi) || (!t.ilanTarihi && ((t.eklenmeZamani || '').includes('az') || (t.eklenmeZamani || '').includes('saat'))) + ); return (
diff --git a/src/components/tools/Pricing.jsx b/src/components/tools/Pricing.jsx index 4724a39..88427a4 100644 --- a/src/components/tools/Pricing.jsx +++ b/src/components/tools/Pricing.jsx @@ -5,7 +5,7 @@ import { Check, Sparkles, CreditCard, Loader2, Info, ShieldCheck, XCircle } from import { CancelSubscriptionModal } from '../CancelSubscriptionModal'; export const Pricing = () => { - const { authUser, plan, planYenile } = useApp(); + const { authUser, plan, planYenile, isAdmin } = useApp(); const [data, setData] = useState(null); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); @@ -75,7 +75,7 @@ export const Pricing = () => {
{error}
)} - {data && !data.configured && ( + {data && !data.configured && isAdmin && (

diff --git a/src/components/tools/PwaSettings.jsx b/src/components/tools/PwaSettings.jsx index 701febe..da7750d 100644 --- a/src/components/tools/PwaSettings.jsx +++ b/src/components/tools/PwaSettings.jsx @@ -7,6 +7,20 @@ import { NotifySubscriptions } from './NotifySubscriptions'; export const PwaSettings = () => { const { userProfile, authUser, tenders, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp(); + // Gerçek Cache API sayımı — sahte "102 ihale önbellekte" yerine + const [onbellekSayisi, setOnbellekSayisi] = useState(null); + const [onbellekAdi, setOnbellekAdi] = useState(''); + useEffect(() => { + if (!('caches' in window)) return; + (async () => { + try { + const adlar = await caches.keys(); + let toplam = 0; + for (const ad of adlar) toplam += (await (await caches.open(ad)).keys()).length; + setOnbellekSayisi(toplam); setOnbellekAdi(adlar[adlar.length - 1] || ''); + } catch { setOnbellekSayisi(null); } + })(); + }, []); const [offlineMode, setOfflineMode] = useState(true); const [installed, setInstalled] = useState(false); const [permission, setPermission] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'unsupported'); @@ -113,16 +127,16 @@ export const PwaSettings = () => {

- İhale Kayıtları: - 102 İhale Önbellekte + İhale Kayıtları (bellekte): + {tenders.length.toLocaleString('tr-TR')} ihale

- Şartname PDF'leri: - 18 Belge Çevrimdışı + Önbelleğe alınmış istek: + {onbellekSayisi == null ? '—' : `${onbellekSayisi} kayıt`}

- Son Senkronizasyon: - Bugün 14:45 + Uygulama sürümü önbelleği: + {onbellekAdi || '—'}

diff --git a/src/components/tools/TenderCalendar.jsx b/src/components/tools/TenderCalendar.jsx index a8047ca..681c209 100644 --- a/src/components/tools/TenderCalendar.jsx +++ b/src/components/tools/TenderCalendar.jsx @@ -4,18 +4,17 @@ import { CalendarDays, Clock, MapPin, AlertCircle } from 'lucide-react'; export const TenderCalendar = () => { const { tenders, setActiveTenderDetail } = useApp(); - const [selectedDay, setSelectedDay] = useState('10.08.2026'); + const GUNLER = ['Pazar', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi']; + const ggaayyyy = (d) => `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${d.getFullYear()}`; + // Bugünden başlayan 7 gün; sayılar havuzdaki gerçek ihale tarihlerinden. + const daysList = Array.from({ length: 7 }, (_, i) => { + const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + i); + const date = ggaayyyy(d); + return { date, label: i === 0 ? `Bugün (${GUNLER[d.getDay()]})` : i === 1 ? `Yarın (${GUNLER[d.getDay()]})` : GUNLER[d.getDay()], count: tenders.filter(t => t.tarih === date).length }; + }); + const [selectedDay, setSelectedDay] = useState(daysList[0].date); - const daysList = [ - { date: '10.08.2026', label: 'Bugün (Pazartesi)', count: 3 }, - { date: '11.08.2026', label: 'Salı', count: 4 }, - { date: '12.08.2026', label: 'Çarşamba', count: 2 }, - { date: '13.08.2026', label: 'Perşembe', count: 5 }, - { date: '14.08.2026', label: 'Cuma', count: 3 }, - { date: '15.08.2026', label: 'Cumartesi', count: 1 } - ]; - - const dayTenders = tenders.filter(t => t.tarih === selectedDay || (selectedDay === '10.08.2026' && (t.tarih || '').includes('10'))); + const dayTenders = tenders.filter(t => t.tarih === selectedDay); return (
diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index 15824b4..717f0bf 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -1,4 +1,4 @@ -import React, {createContext, useContext, useState, useEffect, useMemo } from 'react'; +import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react'; import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService'; import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum'; import { getToken } from '../services/auth'; @@ -168,6 +168,15 @@ export const AppProvider = ({ children }) => { // localStorage yalnızca çevrimdışı önbellek; kaynak doğruluk sunucuda. const [plan, setPlan] = useState(null); const [veriSenkron, setVeriSenkron] = useState(false); + // 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) return; @@ -253,7 +262,7 @@ export const AppProvider = ({ children }) => { }); useEffect(() => { yerelKaydet('ihaleist_rakip_izleme', competitorWatchlist); - if (veriSenkron) sunucuyaYaz('rakip_izleme', competitorWatchlist); + if (veriSenkron) ilkYazmaDegil('rakip_izleme', competitorWatchlist); }, [competitorWatchlist, veriSenkron]); // --- Favori kurumlar (localStorage, gerçek) --- @@ -262,7 +271,7 @@ export const AppProvider = ({ children }) => { }); useEffect(() => { yerelKaydet('ihaleist_favori_kurumlar', favoriteKurumlar); - if (veriSenkron) sunucuyaYaz('favori_kurumlar', favoriteKurumlar); + if (veriSenkron) ilkYazmaDegil('favori_kurumlar', favoriteKurumlar); }, [favoriteKurumlar, veriSenkron]); const toggleFavoriteKurum = (kurum) => { const ad = (kurum || '').trim(); @@ -276,13 +285,12 @@ export const AppProvider = ({ children }) => { const saved = localStorage.getItem('ihaleist_ekip'); if (saved) return JSON.parse(saved); } catch { /* yoksay */ } - return [ - { id: 'tm-1', name: 'Hikmet Tanrıverdi', email: 'hikmet044@gmail.com', role: 'Yönetici' }, - ]; + const u = getStoredUser(); + return u ? [{ id: 'tm-1', name: u.name || u.email, email: u.email || '', role: 'Yönetici' }] : []; }); useEffect(() => { yerelKaydet('ihaleist_ekip', teamMembers); - if (veriSenkron) sunucuyaYaz('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)); @@ -309,7 +317,7 @@ export const AppProvider = ({ children }) => { }); useEffect(() => { yerelKaydet('ihaleist_reminder', reminderSettings); - if (veriSenkron) sunucuyaYaz('reminder', reminderSettings); + if (veriSenkron) ilkYazmaDegil('reminder', reminderSettings); }, [reminderSettings, veriSenkron]); // Şu an sessiz saat aralığında mıyız (gece yarısını geçen aralıkları da destekler) @@ -416,11 +424,12 @@ export const AppProvider = ({ children }) => { // 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: 'Hikmet Tanrıverdi', - username: 'tanriverdi', - company: kayitli?.company || 'Tanrıverdi Medikal & Sağlık Teknolojileri A.Ş.', - website: kayitli?.website || 'https://www.tanriverdimedikal.com.tr', + 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 || {}) }, @@ -434,7 +443,7 @@ export const AppProvider = ({ children }) => { scrapedCapabilities: userProfile.scrapedCapabilities, uyumEsigi: userProfile.uyumEsigi, }; yerelKaydet('ihaleist_firma_profil', veri); - if (veriSenkron) sunucuyaYaz('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)) })); @@ -548,7 +557,7 @@ export const AppProvider = ({ children }) => { useEffect(() => { yerelKaydet('ihaleist_saved_filters', savedFilters); - if (veriSenkron) sunucuyaYaz('saved_filters', savedFilters); + if (veriSenkron) ilkYazmaDegil('saved_filters', savedFilters); }, [savedFilters, veriSenkron]); // (Sahte "online kullanıcı" rastgele sayacı kaldırıldı — canlı istatistikler @@ -571,7 +580,7 @@ export const AppProvider = ({ children }) => { ); const takipImza = takipIknler.join('|'); useEffect(() => { - if (veriSenkron) sunucuyaYaz('takip', takipIknler); + if (veriSenkron) ilkYazmaDegil('takip', takipIknler); // eslint-disable-next-line react-hooks/exhaustive-deps }, [takipImza, veriSenkron]); diff --git a/src/services/analytics.js b/src/services/analytics.js index 9a28eb8..74e331c 100644 --- a/src/services/analytics.js +++ b/src/services/analytics.js @@ -77,7 +77,7 @@ export function firmStats(tenders) { .sort((a, b) => b.wins - a.wins || b.total - a.total); const totalMarket = firms.reduce((s, f) => s + f.total, 0); return { - firms: firms.map((f) => ({ ...f, share: totalMarket ? Math.round((f.total / totalMarket) * 100) : 0 })), + firms: firms.map((f) => ({ ...f, share: totalMarket ? Math.round((f.total / totalMarket) * 1000) / 10 : 0 })), awardCount, totalMarket, };