feat: demo özelliklerini gerçekleştir — canlı veri + localStorage
- 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ı)
This commit is contained in:
@@ -1,13 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { Radar, Bell, Plus, ShieldAlert, Award, FileText, CheckCircle2, Trash2, ArrowRight } from 'lucide-react';
|
||||
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 } = useApp();
|
||||
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;
|
||||
@@ -67,6 +80,44 @@ export const CompetitorRadar = () => {
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user