fix: persist competitor tracking and remove unsupported metrics and alerts

This commit is contained in:
2026-09-06 14:12:43 +03:00
parent 463fd06d30
commit 83df706631
7 changed files with 74 additions and 51 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ export const TopNavigation = () => {
{ id: 'sinir', label: 'Sınır Değer', icon: Calculator },
{ id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp },
{ id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' },
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar, badge: 'Canlı Uyar' },
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar },
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
{ id: 'kirmizi_bayrak', label: 'Kırmızı Bayrak', icon: ShieldAlert, badge: 'Şeffaflık' },
+34 -44
View File
@@ -1,11 +1,10 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '../../context/AppContext';
import { Radar, Bell, Plus, ShieldAlert, Award, FileText, CheckCircle2, Trash2, ArrowRight, TrendingUp } from 'lucide-react';
import confetti from 'canvas-confetti';
import { firmStats, formatTL } from '../../services/analytics';
export const CompetitorRadar = () => {
const { competitorWatchlist, setCompetitorWatchlist, setActiveTab, tenders } = useApp();
const { competitorWatchlist, saveCompetitorWatchlist, competitorSaving, competitorError, setActiveTab, tenders } = useApp();
const [newCompetitorName, setNewCompetitorName] = useState('');
const [newSector, setNewSector] = useState('Medikal & Tıbbi Malzeme');
@@ -14,40 +13,30 @@ export const CompetitorRadar = () => {
const watched = new Set(competitorWatchlist.map((c) => c.name.trim().toLocaleLowerCase('tr-TR')));
const addFromReal = (f) => {
if (watched.has(f.name.trim().toLocaleLowerCase('tr-TR'))) return;
setCompetitorWatchlist((prev) => [
{ id: 'cw-' + Date.now(), name: f.name, sector: f.sectors[0] || 'Genel', winCount: f.wins, totalRevenue: f.total, marketShare: f.share, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true },
saveCompetitorWatchlist((prev) => [
{ id: 'cw-' + crypto.randomUUID(), name: f.name, sector: f.sectors[0] || 'Genel', winCount: f.wins, totalRevenue: f.total, marketShare: f.share, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true },
...prev,
]);
confetti({ particleCount: 40 });
};
const handleAddCompetitor = (e) => {
const handleAddCompetitor = async (e) => {
e.preventDefault();
if (!newCompetitorName.trim()) return;
if (!newCompetitorName.trim() || watched.has(newCompetitorName.trim().toLocaleLowerCase('tr-TR'))) return;
const newComp = {
id: 'cw-' + Date.now(),
name: newCompetitorName,
id: 'cw-' + crypto.randomUUID(),
name: newCompetitorName.trim(),
sector: newSector,
winCount: 0,
totalRevenue: 0,
marketShare: 5,
status: 'Aktif İzleniyor',
alertOnBid: true,
alertOnWin: true
};
setCompetitorWatchlist(prev => [newComp, ...prev]);
setNewCompetitorName('');
confetti({ particleCount: 50 });
};
const toggleAlert = (id, field) => {
setCompetitorWatchlist(prev => prev.map(c => c.id === id ? { ...c, [field]: !c[field] } : c));
if(await saveCompetitorWatchlist(prev => [newComp, ...prev]))setNewCompetitorName('');
};
const removeCompetitor = (id) => {
setCompetitorWatchlist(prev => prev.filter(c => c.id !== id));
saveCompetitorWatchlist(prev => prev.filter(c => c.id !== id));
};
return (
@@ -61,12 +50,12 @@ export const CompetitorRadar = () => {
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="font-extrabold text-xl">Rakip Radar & Proaktif İhale Uyarısı</h2>
<h2 className="font-extrabold text-xl">Rakip Radar</h2>
<span className="bg-indigo-500 text-white text-[10px] font-extrabold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
Proaktif Takip
Kayıtlı Firmalar
</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 uygulama içi, e-posta ve Telegram bildirimi alın.</p>
<p className="text-xs text-indigo-200 mt-0.5">Rakip listenizi kaydedin ve yüklenen sonuç/sözleşme verileriyle karşılaştırın. Rakip teklif veya kazanımına özel otomatik bildirim şu anda etkin değil.</p>
</div>
</div>
@@ -119,8 +108,7 @@ export const CompetitorRadar = () => {
</span>
)}
<a href="https://ekapv2.kik.gov.tr/sorgulamalar/yasak-sorgulama" target="_blank" rel="noopener noreferrer"
onClick={() => { try { navigator.clipboard?.writeText(f.name); } catch { /* yoksay */ } }}
title="KİK yasaklılar sorgusunu açar; firma adı panoya kopyalanır (EKAP sorguyu API'den vermiyor)"
title="KİK yasaklılar sorgusunu açar; firma adını sorguya girin"
className="bg-slate-100 text-slate-700 border border-slate-200 px-1.5 py-0.5 rounded font-bold hover:bg-slate-200">
⛔ yasaklı sorgula
</a>
@@ -134,7 +122,7 @@ export const CompetitorRadar = () => {
</div>
<button
onClick={() => addFromReal(f)}
disabled={watched.has(f.name.trim().toLocaleLowerCase('tr-TR'))}
disabled={competitorSaving || watched.has(f.name.trim().toLocaleLowerCase('tr-TR'))}
className="text-[11px] font-bold px-3 py-1.5 rounded-lg border border-indigo-200 text-indigo-700 hover:bg-indigo-50 disabled:opacity-40 shrink-0"
>
{watched.has(f.name.trim().toLocaleLowerCase('tr-TR')) ? 'İzleniyor' : 'Radara Ekle'}
@@ -145,12 +133,15 @@ export const CompetitorRadar = () => {
)}
</div>
{competitorError&&<p role="alert" className="text-sm text-red-700">{competitorError}</p>}
{competitorSaving&&<p role="status" className="text-sm">Rakip listesi kaydediliyor…</p>}
{/* Add New Competitor Form */}
<form onSubmit={handleAddCompetitor} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs flex flex-col md:flex-row gap-3 items-end">
<div className="flex-1 space-y-1.5 w-full">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">İzlenecek Rakip Firma Adı</label>
<input
type="text"
aria-label="İzlenecek rakip firma adı" disabled={competitorSaving}
required
value={newCompetitorName}
onChange={(e) => setNewCompetitorName(e.target.value)}
@@ -162,6 +153,7 @@ export const CompetitorRadar = () => {
<div className="w-full md:w-64 space-y-1.5">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Sektör</label>
<select
aria-label="Rakip sektör" disabled={competitorSaving}
value={newSector}
onChange={(e) => setNewSector(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-xs font-semibold text-slate-900"
@@ -175,7 +167,7 @@ export const CompetitorRadar = () => {
</div>
<button
type="submit"
type="submit" disabled={competitorSaving || !newCompetitorName.trim() || watched.has(newCompetitorName.trim().toLocaleLowerCase('tr-TR'))}
className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-500 text-white font-extrabold text-xs px-6 py-3 rounded-xl flex items-center justify-center gap-2 shadow-md transition-colors shrink-0"
>
<Plus className="w-4 h-4" />
@@ -189,11 +181,13 @@ export const CompetitorRadar = () => {
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-indigo-600" /> Takipteki Rakip Firmalar ({competitorWatchlist.length})
</h3>
<span className="text-xs text-slate-500">Otomatik bildirimler aktif</span>
<span className="text-xs text-slate-500">Yüklenen havuzdaki sonuçlar</span>
</div>
<div className="divide-y divide-slate-100">
{competitorWatchlist.map((comp) => (
{competitorWatchlist.map((comp) => {
const stats=firms.find(f=>f.name.trim().toLocaleLowerCase('tr-TR')===comp.name.trim().toLocaleLowerCase('tr-TR'));
return (
<div key={comp.id} className="p-5 hover:bg-slate-50/80 transition-colors flex flex-col md:flex-row md:items-center justify-between gap-4">
{/* Info */}
@@ -205,36 +199,32 @@ export const CompetitorRadar = () => {
</span>
</div>
<div className="flex items-center gap-4 text-xs text-slate-500 font-medium">
<span>🏆 Kazandığı İhale: <strong className="text-slate-800">{comp.winCount}</strong></span>
<span>💰 Toplam Hacim: <strong className="text-emerald-700">₺{(comp.totalRevenue / 1000000).toFixed(1)}M</strong></span>
<span>📊 Tahmini Pazar Payı: <strong className="text-indigo-900">%{comp.marketShare}</strong></span>
<span>🏆 Kazandığı İhale: <strong className="text-slate-800">{stats?.wins ?? 'Veri yok'}</strong></span>
<span>💰 Toplam Hacim: <strong className="text-emerald-700">{stats ? formatTL(stats.total) : 'Veri yok'}</strong></span>
<span>📊 Yüklü havuzdaki pay: <strong className="text-indigo-900">{stats ? `%${stats.share}` : 'Veri yok'}</strong></span>
</div>
</div>
{/* Notification Toggles & Actions */}
<div className="flex items-center gap-3 shrink-0">
<button
onClick={() => toggleAlert(comp.id, 'alertOnBid')}
className={`text-xs font-extrabold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-colors ${
comp.alertOnBid ? 'bg-amber-50 text-amber-800 border-amber-300' : 'bg-slate-100 text-slate-400 border-slate-200'
}`}
disabled title="Rakip teklif bildirimleri henüz etkin değil"
className="text-xs font-extrabold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 bg-slate-100 text-slate-400 border-slate-200"
>
<Bell className="w-3.5 h-3.5" />
<span>Teklif Verince Uyar</span>
<span>Teklif uyarısı etkin değil</span>
</button>
<button
onClick={() => toggleAlert(comp.id, 'alertOnWin')}
className={`text-xs font-extrabold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-colors ${
comp.alertOnWin ? 'bg-emerald-50 text-emerald-800 border-emerald-300' : 'bg-slate-100 text-slate-400 border-slate-200'
}`}
disabled title="Rakip kazanım bildirimleri henüz etkin değil"
className="text-xs font-extrabold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 bg-slate-100 text-slate-400 border-slate-200"
>
<Award className="w-3.5 h-3.5" />
<span>Kazanırsa Uyar</span>
<span>Kazanım uyarısı etkin değil</span>
</button>
<button
onClick={() => removeCompetitor(comp.id)}
disabled={competitorSaving} aria-label={`${comp.name} radardan çıkar`} onClick={() => removeCompetitor(comp.id)}
className="p-2 text-slate-400 hover:text-red-600 rounded-lg transition-colors"
title="Radardan Çıkar"
>
@@ -243,7 +233,7 @@ export const CompetitorRadar = () => {
</div>
</div>
))}
);})}
</div>
</div>
+17 -5
View File
@@ -166,7 +166,7 @@ export const AppProvider = ({ children }) => {
useEffect(() => {
notificationVersion.current++;notificationMutation.current=null;seenServerNotifications.current.clear();setNotificationError(null);setNotificationBusy(false);
aiRunRef.current=null;setAiAnalyzing(false);setAiJobStatus('');
dataRevisions.current={};setTeamError(null);
dataRevisions.current={};setTeamError(null);setCompetitorError(null);
setVeriSenkron(false); setPlan(null); setSaveError(null);
setFavoriteKurumlar([]); setTeamMembers([]); setSavedFilters([]); setCompetitorWatchlist([]);
setReminderSettings(defaultReminder()); setUserProfile(defaultProfile(authUser));
@@ -351,9 +351,21 @@ export const AppProvider = ({ children }) => {
// Enterprise Module States
// Rakip izleme listesi — kullanıcı gerçek kazanan firma analizinden ekler (hesaba özel sunucu kaydı)
const [competitorWatchlist, setCompetitorWatchlist] = useState([]);
useEffect(() => {
if (veriSenkron) ilkYazmaDegil('rakip_izleme', competitorWatchlist);
}, [competitorWatchlist, veriSenkron]);
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([]);
@@ -810,7 +822,7 @@ export const AppProvider = ({ children }) => {
aiAnalyzing,
aiJobStatus,
competitorWatchlist,
setCompetitorWatchlist,
saveCompetitorWatchlist,competitorSaving,competitorError,
vaultDocuments,
setVaultDocuments,
reminderSettings,