243 lines
13 KiB
React
243 lines
13 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 { firmStats, formatTL } from '../../services/analytics';
|
||
|
||
export const CompetitorRadar = () => {
|
||
const { competitorWatchlist, saveCompetitorWatchlist, competitorSaving, competitorError, 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;
|
||
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,
|
||
]);
|
||
};
|
||
|
||
const handleAddCompetitor = async (e) => {
|
||
e.preventDefault();
|
||
if (!newCompetitorName.trim() || watched.has(newCompetitorName.trim().toLocaleLowerCase('tr-TR'))) return;
|
||
|
||
const newComp = {
|
||
id: 'cw-' + crypto.randomUUID(),
|
||
name: newCompetitorName.trim(),
|
||
sector: newSector,
|
||
status: 'Aktif İzleniyor',
|
||
alertOnBid: true,
|
||
alertOnWin: true
|
||
};
|
||
|
||
if(await saveCompetitorWatchlist(prev => [newComp, ...prev]))setNewCompetitorName('');
|
||
};
|
||
|
||
const removeCompetitor = (id) => {
|
||
saveCompetitorWatchlist(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</h2>
|
||
<span className="bg-indigo-500 text-white text-[10px] font-extrabold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
|
||
Kayıtlı Firmalar
|
||
</span>
|
||
</div>
|
||
<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>
|
||
|
||
<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>
|
||
{/* Kazanan istihbaratı: ne kadar aşağı veriyor, kaç rakibe karşı kazanıyor */}
|
||
<div className="text-[11px] mt-1 ml-7 flex flex-wrap gap-1.5">
|
||
{f.kirimOrani != null && (
|
||
<span className="bg-emerald-50 text-emerald-800 border border-emerald-200 px-1.5 py-0.5 rounded font-bold"
|
||
title="Sözleşme bedelinin yaklaşık maliyete göre ortalama kırımı">
|
||
↓ %{f.kirimOrani} kırım
|
||
</span>
|
||
)}
|
||
{f.ortTeklif != null && (
|
||
<span className="bg-blue-50 text-blue-800 border border-blue-200 px-1.5 py-0.5 rounded font-bold"
|
||
title="Kazandığı ihalelerde ortalama geçerli teklif sayısı">
|
||
⚔ ort. {f.ortTeklif} teklif
|
||
</span>
|
||
)}
|
||
<a href="https://ekapv2.kik.gov.tr/sorgulamalar/yasak-sorgulama" target="_blank" rel="noopener noreferrer"
|
||
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>
|
||
{f.tekTeklifOrani != null && f.tekTeklifOrani > 0 && (
|
||
<span className="bg-amber-50 text-amber-800 border border-amber-200 px-1.5 py-0.5 rounded font-bold"
|
||
title="Tek teklifle (rakipsiz) kazandığı ihalelerin oranı">
|
||
%{f.tekTeklifOrani} tek teklif
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => addFromReal(f)}
|
||
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'}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</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)}
|
||
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
|
||
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"
|
||
>
|
||
<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" 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" />
|
||
<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">Yüklenen havuzdaki sonuçlar</span>
|
||
</div>
|
||
|
||
<div className="divide-y divide-slate-100">
|
||
{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 */}
|
||
<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">{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
|
||
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 uyarısı etkin değil</span>
|
||
</button>
|
||
|
||
<button
|
||
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ım uyarısı etkin değil</span>
|
||
</button>
|
||
|
||
<button
|
||
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"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
);})}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
};
|