Kullanıcı testi hataları: sabit tarihler, kişisel varsayılanlar, kılavuz düğmesi, mobil başlık, AI taslak çökmesi

- Günlük Bülten ve İhale Takvimi sabit "10.08.2026" yerine bugünden hesaplar;
  takvim 7 günü gerçek ihale tarihleriyle sayar
- Ekip/üye/firma varsayılanları oturumdaki kullanıcıdan; sahte "Ekip Notları"
  ve sabit "%94 Uyum" rozeti kaldırıldı (gerçek uyum skoru varsa gösterilir)
- Kılavuz düğmesi document.body'ye portal (ekran dışına düşüyordu)
- Detay modalı havuzdaki güncel kaydı okur: Takip Et / Görev aç etiketleri güncellenir
- Mobil: başlıkta ikincil düğmeler md altında gizli, arama formu küçülebilir
- PWA sayfası: sahte önbellek sayıları yerine Cache API ve bellek sayımı
- Sözleşme sorgula pazar payı bir ondalık (hep %0 görünüyordu)
- Rakip Radar "SMS" vaadi düzeltildi; sektör=CPV etiketi tekrarı gizlendi;
  Stripe kurulum uyarısı yalnız yöneticiye
- Açılışta 7 gereksiz PUT /api/me/data: ilk yükleme yazımı atlanır
- server: teklif taslağı çökmesi (kalemOku nesne döndürür), denetim yanıtından
  model adı kaldırıldı, gereklilikler satır bazlı; derin analiz 12k token +
  kesik JSON onarımı

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 09:43:41 +03:00
co-authored by Claude Fable 5.1
parent 6d7d938711
commit 08c0a0af7e
14 changed files with 130 additions and 60 deletions
+14 -5
View File
@@ -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 };
}
+34 -2
View File
@@ -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 = [];
+4 -4
View File
@@ -54,7 +54,7 @@ export const Header = () => {
</div>
{/* Search Bar */}
<form onSubmit={handleSearchSubmit} className="flex-1 max-w-2xl flex items-center gap-2">
<form onSubmit={handleSearchSubmit} className="flex-1 min-w-0 max-w-2xl flex items-center gap-2">
<div className="relative flex-1">
<input
type="text"
@@ -100,13 +100,13 @@ export const Header = () => {
</form>
{/* Right Action Icons */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
{/* Real Live EKAP Mode Badge */}
<button
onClick={refreshLiveEKAPData}
disabled={isRefreshingLive}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-extrabold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-xs transition-colors shrink-0 disabled:opacity-50"
className="bg-emerald-600 hover:bg-emerald-500 text-white font-extrabold px-3 py-1.5 rounded-lg text-xs hidden md:hidden md:flex items-center gap-1.5 shadow-xs transition-colors shrink-0 disabled:opacity-50"
title="Canlı EKAP ve Doğrudan Temin Verilerini Anlık Güncelleyin"
>
<RefreshCw className={`w-3.5 h-3.5 text-white ${isRefreshingLive ? 'animate-spin' : ''}`} />
@@ -126,7 +126,7 @@ export const Header = () => {
{/* Company Web Scanner Button */}
<button
onClick={() => setIsCompanyScannerOpen(true)}
className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-xs transition-colors shrink-0"
className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-3 py-1.5 rounded-lg text-xs hidden md:flex items-center gap-1.5 shadow-xs transition-colors shrink-0"
title="Web Sitenizi Tarayın & AI İhale Profiline İşleyin"
>
<Globe className="w-4 h-4 text-blue-200" />
+4 -2
View File
@@ -1,3 +1,4 @@
import { createPortal } from 'react-dom';
import React, { useEffect, useState } from 'react';
import { useApp } from '../context/AppContext';
import { HelpCircle, X, BookOpen, Lightbulb, ListChecks } from 'lucide-react';
@@ -97,7 +98,7 @@ export const KullanimKilavuzu = () => {
</div>
) : null;
return (
return createPortal(
<>
<button
type="button"
@@ -142,6 +143,7 @@ export const KullanimKilavuzu = () => {
</aside>
</div>
)}
</>
</>,
document.body,
);
};
+1 -1
View File
@@ -137,7 +137,7 @@ export const TenderCard = ({ tender }) => {
<span className="bg-slate-100 text-slate-700 font-medium text-[11px] px-2.5 py-1 rounded-md border border-slate-200">
{tender.sektor}
</span>
{tender.cpvLabel && tender.cpvLabel !== 'Genel' && (
{tender.cpvLabel && tender.cpvLabel !== 'Genel' && tender.cpvLabel !== tender.sektor && (
<span className="bg-sky-50 text-sky-700 font-medium text-[11px] px-2.5 py-1 rounded-md border border-sky-200">
{tender.cpvLabel}
</span>
+5 -6
View File
@@ -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 => {
+1 -1
View File
@@ -66,7 +66,7 @@ export const CompetitorRadar = () => {
Proaktif Takip
</span>
</div>
<p className="text-xs text-indigo-200 mt-0.5">İzlemeye aldığınız rakip firmalar ihaleye teklif verdiğinde veya kazandığında anında SMS ve Push bildirimi alın.</p>
<p className="text-xs text-indigo-200 mt-0.5">İzlemeye aldığınız rakip firmalar ihaleye teklif verdiğinde veya kazandığında uygulama içi, e-posta ve Telegram bildirimi alın.</p>
</div>
</div>
+2 -2
View File
@@ -75,10 +75,10 @@ export const ContractSearch = () => {
<div>
<div className="flex justify-between text-xs font-semibold text-slate-700 mb-1">
<span>Pazar Payı (hacim)</span>
<span className="font-bold text-indigo-700">%{comp.share}</span>
<span className="font-bold text-indigo-700">%{Number(comp.share).toLocaleString('tr-TR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
<div className="bg-indigo-600 h-2 rounded-full" style={{ width: `${Math.max(comp.share, 2)}%` }}></div>
<div className="bg-indigo-600 h-2 rounded-full" style={{ width: `${Math.min(100, Math.max(comp.share * 4, 2))}%` }}></div>
</div>
</div>
</div>
+8 -2
View File
@@ -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 (
<div className="space-y-4 my-4 animate-fade-in">
+2 -2
View File
@@ -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 = () => {
<div className="bg-red-50 border border-red-200 rounded-xl p-3 text-xs text-red-800 text-center">{error}</div>
)}
{data && !data.configured && (
{data && !data.configured && isAdmin && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
<Info className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<p className="text-xs text-amber-900 leading-relaxed">
+20 -6
View File
@@ -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 = () => {
<div className="space-y-2 text-xs text-slate-700">
<p className="flex justify-between">
<span>İhale Kayıtları:</span>
<strong className="text-slate-900">102 İhale Önbellekte</strong>
<span>İhale Kayıtları (bellekte):</span>
<strong className="text-slate-900">{tenders.length.toLocaleString('tr-TR')} ihale</strong>
</p>
<p className="flex justify-between">
<span>Şartname PDF'leri:</span>
<strong className="text-slate-900">18 Belge Çevrimdışı</strong>
<span>Önbelleğe alınmış istek:</span>
<strong className="text-slate-900">{onbellekSayisi == null ? '—' : `${onbellekSayisi} kayıt`}</strong>
</p>
<p className="flex justify-between">
<span>Son Senkronizasyon:</span>
<strong className="text-emerald-700">Bugün 14:45</strong>
<span>Uygulama sürümü önbelleği:</span>
<strong className="text-emerald-700">{onbellekAdi || '—'}</strong>
</p>
</div>
+10 -11
View File
@@ -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 (
<div className="space-y-4 my-4 animate-fade-in">
+24 -15
View File
@@ -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]);
+1 -1
View File
@@ -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,
};