fix: persist competitor tracking and remove unsupported metrics and alerts
This commit is contained in:
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user