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:
2026-08-28 01:58:46 +03:00
parent 512d529af2
commit 0ab74143c0
16 changed files with 895 additions and 154 deletions
+80
View File
@@ -0,0 +1,80 @@
import React, { useMemo } from 'react';
import { useApp } from '../../context/AppContext';
import { MapPin } from 'lucide-react';
import { provinceDensity, normProvince } from '../../services/analytics';
const ILLER = [
'Adana','Adıyaman','Afyonkarahisar','Ağrı','Amasya','Ankara','Antalya','Artvin','Aydın','Balıkesir',
'Bilecik','Bingöl','Bitlis','Bolu','Burdur','Bursa','Çanakkale','Çankırı','Çorum','Denizli',
'Diyarbakır','Edirne','Elazığ','Erzincan','Erzurum','Eskişehir','Gaziantep','Giresun','Gümüşhane','Hakkari',
'Hatay','Isparta','Mersin','İstanbul','İzmir','Kars','Kastamonu','Kayseri','Kırklareli','Kırşehir',
'Kocaeli','Konya','Kütahya','Malatya','Manisa','Kahramanmaraş','Mardin','Muğla','Muş','Nevşehir',
'Niğde','Ordu','Rize','Sakarya','Samsun','Siirt','Sinop','Sivas','Tekirdağ','Tokat',
'Trabzon','Tunceli','Şanlıurfa','Uşak','Van','Yozgat','Zonguldak','Aksaray','Bayburt','Karaman',
'Kırıkkale','Batman','Şırnak','Bartın','Ardahan','Iğdır','Yalova','Karabük','Kilis','Osmaniye','Düzce',
];
function heat(count, max) {
if (!count) return { bg: '#f1f5f9', fg: '#94a3b8' };
const r = count / (max || 1);
if (r > 0.66) return { bg: '#b91c1c', fg: '#fff' };
if (r > 0.4) return { bg: '#ef4444', fg: '#fff' };
if (r > 0.2) return { bg: '#f59e0b', fg: '#111' };
if (r > 0.08) return { bg: '#fcd34d', fg: '#111' };
return { bg: '#dbeafe', fg: '#1e40af' };
}
export const TurkeyHeatMap = () => {
const { tenders, setActiveTab, setSelectedCity } = useApp();
const density = useMemo(() => provinceDensity(tenders), [tenders]);
const max = Math.max(1, ...density.values());
const withData = ILLER.filter((il) => density.get(normProvince(il)));
const topPct = Math.round((withData.length / 81) * 100);
const openCity = (il) => {
setSelectedCity(il);
setActiveTab('list');
};
return (
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3 flex-wrap gap-2">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<MapPin className="w-4 h-4 text-red-600" /> İl Bazlı İhale Yoğunluk Haritası — Canlı
</h3>
<p className="text-xs text-slate-500">Yüklü ihalelerin illere dağılımı. Bir ile tıklayınca liste o ile filtrelenir. {withData.length}/81 ilde aktif ihale (%{topPct}).</p>
</div>
<div className="flex items-center gap-1.5 text-[10px] text-slate-500">
<span>Az</span>
<span className="w-4 h-3 rounded" style={{ background: '#dbeafe' }} />
<span className="w-4 h-3 rounded" style={{ background: '#fcd34d' }} />
<span className="w-4 h-3 rounded" style={{ background: '#f59e0b' }} />
<span className="w-4 h-3 rounded" style={{ background: '#ef4444' }} />
<span className="w-4 h-3 rounded" style={{ background: '#b91c1c' }} />
<span>Çok</span>
</div>
</div>
<div className="grid grid-cols-3 sm:grid-cols-6 md:grid-cols-9 gap-1.5">
{ILLER.map((il) => {
const c = density.get(normProvince(il)) || 0;
const { bg, fg } = heat(c, max);
return (
<button
key={il}
onClick={() => c && openCity(il)}
disabled={!c}
title={`${il}: ${c} ihale`}
className={`rounded-lg p-2 text-center transition-transform ${c ? 'hover:scale-105 cursor-pointer' : 'cursor-default opacity-70'}`}
style={{ background: bg, color: fg }}
>
<div className="text-[10px] font-bold leading-tight truncate">{il}</div>
<div className="text-[13px] font-black leading-none mt-0.5">{c || '·'}</div>
</button>
);
})}
</div>
</div>
);
};