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:
+14
-5
@@ -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
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user