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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user