Files
ihaleist/src/components/tools/CompetitorRadar.jsx
T

243 lines
13 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};