- analytics.js: firmStats/itemStats/provinceDensity (gerçek hesaplama) - Rakip Radar: canlı veriden kazanan firma analizi (mock 3 firma kaldırıldı) - Fiyat Analizi: gerçek kalem min/medyan/ort/maks + CSV export - TurkeyHeatMap: 81 il gerçek interaktif yoğunluk haritası (tıkla→filtrele) - Favori kurumlar: kart yıldızı + hızlı filtre (localStorage) - Ekip yönetimi: gerçek CRUD (RightSidebar) + Kanban ataması ekipten - Bildirimler: gerçek Notification API + izin + test + sessiz saatler - PDF: jsPDF ile gerçek birim fiyat teklif cetveli (kaşe/imza alanı)
226 lines
11 KiB
React
226 lines
11 KiB
React
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 [newCompetitorName, setNewCompetitorName] = useState('');
|
||
const [newSector, setNewSector] = useState('Medikal & Tıbbi Malzeme');
|
||
|
||
// Gerçek kazanan-firma analizi (canlı veriden)
|
||
const { firms, awardCount, totalMarket } = useMemo(() => firmStats(tenders), [tenders]);
|
||
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 },
|
||
...prev,
|
||
]);
|
||
confetti({ particleCount: 40 });
|
||
};
|
||
|
||
const handleAddCompetitor = (e) => {
|
||
e.preventDefault();
|
||
if (!newCompetitorName.trim()) return;
|
||
|
||
const newComp = {
|
||
id: 'cw-' + Date.now(),
|
||
name: newCompetitorName,
|
||
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));
|
||
};
|
||
|
||
const removeCompetitor = (id) => {
|
||
setCompetitorWatchlist(prev => prev.filter(c => c.id !== id));
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6 animate-fade-in">
|
||
|
||
{/* Header Banner */}
|
||
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-blue-900 text-white p-6 rounded-2xl shadow-lg border border-indigo-800/40 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center font-black shadow-md">
|
||
<Radar className="w-6 h-6 animate-pulse" />
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="font-extrabold text-xl">Rakip Radar & Proaktif İhale Uyarısı</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
|
||
</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>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => setActiveTab('sozlesme')}
|
||
className="bg-white/10 hover:bg-white/20 text-white font-bold text-xs px-4 py-2.5 rounded-xl border border-white/20 transition-colors flex items-center gap-2 shrink-0"
|
||
>
|
||
<FileText className="w-4 h-4 text-amber-400" />
|
||
<span>Sözleşme Sorgulama İncele</span>
|
||
<ArrowRight className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* GERÇEK: Piyasadaki Kazanan Firmalar (canlı veriden hesaplanır) */}
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between flex-wrap gap-2">
|
||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||
<TrendingUp className="w-4 h-4 text-emerald-600" /> Kazanan Firma Analizi — Canlı Veri
|
||
</h3>
|
||
<span className="text-xs text-slate-500">{awardCount} sonuçlanmış ihale · toplam {formatTL(totalMarket)}</span>
|
||
</div>
|
||
{firms.length === 0 ? (
|
||
<div className="p-8 text-center text-sm text-slate-500">
|
||
Henüz kazanan firma verisi birikmedi. Doğrudan temin / sonuç ilanları geldikçe bu tablo gerçek kazanan firmalar ve sözleşme tutarlarıyla otomatik dolar.
|
||
</div>
|
||
) : (
|
||
<div className="divide-y divide-slate-100">
|
||
{firms.slice(0, 12).map((f, i) => (
|
||
<div key={f.name} className="px-6 py-3 flex items-center justify-between gap-3 hover:bg-slate-50/70">
|
||
<div className="min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs font-black text-slate-400 w-5">{i + 1}</span>
|
||
<span className="font-bold text-sm text-slate-900 truncate">{f.name}</span>
|
||
</div>
|
||
<div className="text-[11px] text-slate-500 ml-7">
|
||
🏆 {f.wins} ihale · 💰 {formatTL(f.total)} {f.share ? `· %${f.share} pay` : ''} {f.sectors[0] ? `· ${f.sectors[0]}` : ''}
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => addFromReal(f)}
|
||
disabled={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'}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 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"
|
||
required
|
||
value={newCompetitorName}
|
||
onChange={(e) => setNewCompetitorName(e.target.value)}
|
||
placeholder="Örn: ABC Medikal Sanayi ve Ticaret A.Ş."
|
||
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-xs font-semibold text-slate-900 focus:ring-2 focus:ring-indigo-500"
|
||
/>
|
||
</div>
|
||
|
||
<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
|
||
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"
|
||
>
|
||
<option>Medikal & Tıbbi Malzeme</option>
|
||
<option>Sağlık & Büro Malzemeleri</option>
|
||
<option>Laboratuvar & Bilişim</option>
|
||
<option>Kırtasiye & Sanatsal</option>
|
||
<option>Temizlik & Hijyen</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
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" />
|
||
<span>Radara Ekle</span>
|
||
</button>
|
||
</form>
|
||
|
||
{/* Tracked Competitors Table */}
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
|
||
<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>
|
||
</div>
|
||
|
||
<div className="divide-y divide-slate-100">
|
||
{competitorWatchlist.map((comp) => (
|
||
<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 */}
|
||
<div className="space-y-1">
|
||
<div className="flex items-center gap-2">
|
||
<h4 className="font-bold text-sm text-slate-900">{comp.name}</h4>
|
||
<span className="bg-indigo-100 text-indigo-800 font-bold text-[10px] px-2 py-0.5 rounded-full">
|
||
{comp.sector}
|
||
</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>
|
||
</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'
|
||
}`}
|
||
>
|
||
<Bell className="w-3.5 h-3.5" />
|
||
<span>Teklif Verince Uyar</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'
|
||
}`}
|
||
>
|
||
<Award className="w-3.5 h-3.5" />
|
||
<span>Kazanırsa Uyar</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => removeCompetitor(comp.id)}
|
||
className="p-2 text-slate-400 hover:text-red-600 rounded-lg transition-colors"
|
||
title="Radardan Çıkar"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
};
|