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
+3 -26
View File
@@ -2,6 +2,7 @@ import React from 'react';
import { useApp } from '../../context/AppContext';
import { BarChart3, TrendingUp, Trophy, Target, MapPin, Building2, Award, ArrowUpRight, DollarSign } from 'lucide-react';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, PieChart, Pie, Cell } from 'recharts';
import { TurkeyHeatMap } from './TurkeyHeatMap';
export const AnalyticsDashboard = () => {
const { tenders, userProfile } = useApp();
@@ -148,32 +149,8 @@ export const AnalyticsDashboard = () => {
</div>
{/* City Price Heatmap Section */}
<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">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<MapPin className="w-4 h-4 text-red-600" /> Şehir & Kurum Bazlı İhale Yoğunluk Isı Haritası
</h3>
<p className="text-xs text-slate-500">Bölgelere göre ihale sayıları ve ortalama idare yaklaşık maliyet seviyeleri.</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-3">
{cityHeatmap.map(item => (
<div key={item.city} className="bg-slate-50 p-4 rounded-xl border border-slate-200 space-y-2">
<div className="flex items-center justify-between">
<span className="font-extrabold text-sm text-slate-900">{item.city}</span>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full ${item.color}`}>
{item.yogunluk}
</span>
</div>
<p className="text-xs text-slate-600 font-medium">İhale Sayısı: <strong className="text-slate-900">{item.ihaleSayisi}</strong></p>
<p className="text-xs text-slate-600 font-medium">Ort. Bedel: <strong className="text-indigo-900">{item.ortalamaMaliyet}</strong></p>
</div>
))}
</div>
</div>
{/* İl bazlı gerçek yoğunluk haritası */}
<TurkeyHeatMap />
</div>
);
+54 -3
View File
@@ -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">
+97 -90
View File
@@ -1,130 +1,137 @@
import React, { useState } from 'react';
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
import { TrendingUp, Search, Download, Sparkles, AlertCircle } from 'lucide-react';
import React, { useState, useMemo } from 'react';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Cell } from 'recharts';
import { TrendingUp, Download, Sparkles, Inbox } from 'lucide-react';
import { useApp } from '../../context/AppContext';
import { itemStats, formatTL } from '../../services/analytics';
import confetti from 'canvas-confetti';
export const PriceAnalysis = () => {
const [selectedProduct, setSelectedProduct] = useState('35*50 TUVAL');
const { tenders } = useApp();
const { items, itemCount, uniqueItems } = useMemo(() => itemStats(tenders), [tenders]);
const [selectedKey, setSelectedKey] = useState(null);
const priceHistoryData = [
{ month: 'Ocak 2026', minFiyat: 110, ortFiyat: 125, maksFiyat: 140 },
{ month: 'Şubat 2026', minFiyat: 115, ortFiyat: 130, maksFiyat: 145 },
{ month: 'Mart 2026', minFiyat: 120, ortFiyat: 135, maksFiyat: 155 },
{ month: 'Nisan 2026', minFiyat: 125, ortFiyat: 140, maksFiyat: 160 },
{ month: 'Mayıs 2026', minFiyat: 130, ortFiyat: 145, maksFiyat: 170 },
{ month: 'Haziran 2026', minFiyat: 135, ortFiyat: 150, maksFiyat: 180 },
{ month: 'Temmuz 2026', minFiyat: 140, ortFiyat: 155, maksFiyat: 185 },
{ month: 'Ağustos 2026', minFiyat: 145, ortFiyat: 160, maksFiyat: 190 }
];
const selected = useMemo(() => {
if (!items.length) return null;
const found = items.find((i) => i.ad === selectedKey);
return found || items[0];
}, [items, selectedKey]);
const productsList = [
'35*50 TUVAL',
'50*70 TUVAL',
'1 Gözlü Yemek Kabı',
'Kablosuz Panik Butonu',
'G4 Panel Filtre',
'Çamaşır Suyu 20 L',
'Revizyon Kalça Protez Femoral Stem'
];
const chartData = selected
? [
{ label: 'En Düşük', value: Math.round(selected.min), renk: '#2563eb' },
{ label: 'Medyan', value: Math.round(selected.median), renk: '#059669' },
{ label: 'Ortalama', value: Math.round(selected.avg), renk: '#f59e0b' },
{ label: 'En Yüksek', value: Math.round(selected.max), renk: '#dc2626' },
]
: [];
const handleDownloadReport = () => {
confetti({ particleCount: 70 });
alert(`${selectedProduct} Fiyat Trend Analizi Raporu (PDF/CSV) İndirildi.`);
if (!items.length) return;
const headers = ['Kalem', 'Birim', 'Adet(kayıt)', 'Min', 'Medyan', 'Ortalama', 'Maks'];
const rows = items.map((i) => [
`"${(i.ad || '').replace(/"/g, '""')}"`, i.birim || '', i.count,
Math.round(i.min), Math.round(i.median), Math.round(i.avg), Math.round(i.max),
]);
const csv = 'data:text/csv;charset=utf-8,' + [headers.join(','), ...rows.map((r) => r.join(','))].join('\n');
const link = document.createElement('a');
link.href = encodeURI(csv);
link.download = `ihaleist_fiyat_analizi_${Date.now()}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
confetti({ particleCount: 60 });
};
return (
<div className="space-y-4 my-4 animate-fade-in">
{/* Top Banner */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center shadow-md">
<TrendingUp className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Birim Fiyat Analizi & Trend Grafikleri</h1>
<p className="text-xs text-slate-500">Ürün bazında geçmiş ihalelerdeki birim fiyat trendini ve ortalama teklif marjlarını inceleyin.</p>
<h1 className="text-xl font-black text-slate-900">Kalem Bazlı Fiyat Analizi</h1>
<p className="text-xs text-slate-500">
Toplanan ihalelerdeki gerçek birim fiyatlardan hesaplanır · <strong>{uniqueItems}</strong> farklı kalem, <strong>{itemCount}</strong> fiyat kaydı.
</p>
</div>
</div>
<button
onClick={handleDownloadReport}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-4 py-2 rounded-lg flex items-center gap-2 shadow-xs shrink-0"
disabled={!items.length}
className="bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 text-white font-bold text-xs px-4 py-2 rounded-lg flex items-center gap-2 shadow-xs shrink-0"
>
<Download className="w-4 h-4" /> Rapor İndir (PDF/CSV)
<Download className="w-4 h-4" /> CSV İndir
</button>
</div>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column: Product Selector */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs space-y-4 h-fit">
<h3 className="font-bold text-xs uppercase tracking-wider text-slate-800 border-b border-slate-100 pb-2">
Ürün / Malzeme Seçimi
</h3>
<div className="space-y-1.5">
{productsList.map(prod => (
{items.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-10 text-center text-sm text-slate-500 flex flex-col items-center gap-2">
<Inbox className="w-8 h-8 text-slate-300" />
Henüz kalem fiyat verisi birikmedi. Kalem/birim fiyat içeren ihaleler (doğrudan temin sözleşme kalemleri) geldikçe bu analiz gerçek fiyatlarla otomatik dolar.
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs space-y-2 h-fit max-h-[520px] overflow-y-auto">
<h3 className="font-bold text-xs uppercase tracking-wider text-slate-800 border-b border-slate-100 pb-2 sticky top-0 bg-white">
Kalemler ({items.length})
</h3>
{items.slice(0, 60).map((it) => (
<button
key={prod}
onClick={() => setSelectedProduct(prod)}
className={`w-full text-left p-3 rounded-lg text-xs font-semibold transition-all ${
selectedProduct === prod
? 'bg-emerald-600 text-white shadow-xs'
: 'bg-slate-50 hover:bg-slate-100 text-slate-700 border border-slate-200'
key={it.ad}
onClick={() => setSelectedKey(it.ad)}
className={`w-full text-left p-2.5 rounded-lg text-xs font-semibold transition-all ${
selected?.ad === it.ad ? 'bg-emerald-600 text-white' : 'bg-slate-50 hover:bg-slate-100 text-slate-700 border border-slate-200'
}`}
>
📦 {prod}
<div className="truncate">📦 {it.ad}</div>
<div className={`text-[10px] ${selected?.ad === it.ad ? 'text-emerald-100' : 'text-slate-400'}`}>{it.count} kayıt · medyan {formatTL(it.median)}</div>
</button>
))}
</div>
</div>
{/* Right 2 Columns: Recharts Chart & AI Assistant */}
<div className="lg:col-span-2 space-y-6">
{/* Recharts Line Chart */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-bold text-sm text-slate-900">
{selectedProduct} — 8 Aylık Birim Fiyat Değişim Grafiği (TL)
</h3>
<span className="text-[10px] bg-emerald-100 text-emerald-800 font-extrabold px-2 py-0.5 rounded-full">
Canlı EKAP Verisi
</span>
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs space-y-4">
<div className="flex items-center justify-between gap-2 flex-wrap">
<h3 className="font-bold text-sm text-slate-900 truncate">{selected?.ad} — Birim Fiyat Dağılımı (₺)</h3>
<span className="text-[10px] bg-emerald-100 text-emerald-800 font-extrabold px-2 py-0.5 rounded-full">{selected?.count} gerçek kayıt</span>
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="label" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip formatter={(v) => formatTL(v)} />
<Bar dataKey="value" radius={[6, 6, 0, 0]}>
{chartData.map((d, i) => <Cell key={i} fill={d.renk} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-4 gap-2 text-center">
{chartData.map((d) => (
<div key={d.label} className="bg-slate-50 rounded-lg p-2 border border-slate-100">
<div className="text-[10px] text-slate-500">{d.label}</div>
<div className="text-sm font-black" style={{ color: d.renk }}>{formatTL(d.value)}</div>
</div>
))}
</div>
</div>
<div className="h-72 w-full pt-2">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={priceHistoryData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip />
<Line type="monotone" dataKey="ortFiyat" stroke="#059669" strokeWidth={3} name="Ortalama Fiyat" />
<Line type="monotone" dataKey="minFiyat" stroke="#2563eb" strokeWidth={2} strokeDasharray="5 5" name="En Düşük Fiyat" />
<Line type="monotone" dataKey="maksFiyat" stroke="#dc2626" strokeWidth={2} strokeDasharray="5 5" name="En Yüksek Fiyat" />
</LineChart>
</ResponsiveContainer>
<div className="bg-linear-to-r from-slate-900 to-emerald-950 text-white p-5 rounded-xl space-y-2 shadow-md border border-emerald-900/40">
<div className="flex items-center gap-2 font-bold text-sm text-emerald-400">
<Sparkles className="w-5 h-5 text-amber-400" /> Teklif Önerisi (gerçek veriye dayalı)
</div>
<p className="text-xs text-slate-300 leading-relaxed">
<strong>{selected?.ad}</strong> için toplanan {selected?.count} gerçek kayda göre rekabetçi birim fiyat aralığınız{' '}
<strong className="text-amber-300">{formatTL(selected?.min)} – {formatTL(selected?.median)}</strong> arasıdır.
Medyanın altında teklif vermek kazanma olasılığını artırır; ortalamanın ({formatTL(selected?.avg)}) üstü riskli olabilir.
</p>
</div>
</div>
{/* AI Pricing Advice Box */}
<div className="bg-linear-to-r from-slate-900 to-emerald-950 text-white p-5 rounded-xl space-y-3 shadow-md border border-emerald-900/40">
<div className="flex items-center gap-2 font-bold text-sm text-emerald-400">
<Sparkles className="w-5 h-5 text-amber-400" />
<span>Yapay Zeka Teklif Öneri Motoru</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed">
<strong>{selectedProduct}</strong> için son 8 aydaki ihale kazanma verilerine göre optimal teklif birim fiyatınız: <strong className="text-amber-300 text-sm">₺155 - ₺165</strong> aralığıdır. Bu aralıkta kazanma olasılığınız <strong>%88</strong> olarak hesaplanmıştır.
</p>
</div>
</div>
</div>
)}
</div>
);
};
+58 -17
View File
@@ -1,13 +1,24 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, ShieldCheck, Zap, Sparkles } from 'lucide-react';
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, ShieldCheck, Zap, Sparkles, Moon } from 'lucide-react';
import confetti from 'canvas-confetti';
export const PwaSettings = () => {
const { userProfile } = useApp();
const { userProfile, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp();
const [offlineMode, setOfflineMode] = useState(true);
const [pushSubscribed, setPushSubscribed] = useState(true);
const [installed, setInstalled] = useState(false);
const [permission, setPermission] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'unsupported');
const handleEnablePush = async () => {
const p = await requestPushPermission();
setPermission(p);
if (p === 'granted') {
setReminderSettings((s) => ({ ...s, pushEnabled: true }));
notify('Bildirimler açıldı', 'Yeni ihale ve son tarih uyarıları buradan gelecek.');
confetti({ particleCount: 50 });
}
};
const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch }));
const handleInstallPwa = () => {
setInstalled(true);
@@ -67,30 +78,60 @@ export const PwaSettings = () => {
</button>
</div>
{/* Mobile Push Notifications */}
{/* Gerçek Tarayıcı Bildirimleri + Sessiz Saatler */}
<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">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<Bell className="w-4 h-4 text-amber-500" /> Anlık Mobil Push Bildirimleri
<Bell className="w-4 h-4 text-amber-500" /> Anlık Bildirimler
</h3>
<span className="bg-amber-100 text-amber-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full">
İzin Verildi
<span className={`font-extrabold text-[10px] px-2 py-0.5 rounded-full ${
permission === 'granted' ? 'bg-emerald-100 text-emerald-800' : permission === 'denied' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-600'
}`}>
{permission === 'granted' ? 'İzin Verildi' : permission === 'denied' ? 'Reddedildi' : permission === 'unsupported' ? 'Desteklenmiyor' : 'İzin Bekliyor'}
</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed">
İhale son başvuru saatine 3 saat kala mobil cihazınıza sesli ve titreşimli anlık uyarı bildirimi gönderilir.
Tarayıcı bildirimlerine izin verin; yeni ihale ve son tarih uyarıları gerçek bildirim olarak gelir.
</p>
<label className="flex items-center justify-between p-3 bg-slate-50 rounded-xl border border-slate-200 cursor-pointer text-xs font-bold text-slate-800">
<span>Mobil Push Uyarısı</span>
<input
type="checkbox"
checked={pushSubscribed}
onChange={() => setPushSubscribed(!pushSubscribed)}
className="w-4 h-4 accent-cyan-600 rounded"
/>
</label>
{permission !== 'granted' ? (
<button onClick={handleEnablePush} disabled={permission === 'unsupported'}
className="w-full bg-amber-500 hover:bg-amber-400 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl">
Bildirimlere İzin Ver
</button>
) : (
<button onClick={() => notify('Test bildirimi', isQuietNow() ? 'Sessiz saatte — sadece listeye eklendi.' : 'Bildirimler çalışıyor ✓')}
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs py-2.5 rounded-xl">
Test Bildirimi Gönder
</button>
)}
{/* Sessiz saatler — gerçek */}
<div className="pt-2 border-t border-slate-100 space-y-2">
<label className="flex items-center justify-between text-xs font-bold text-slate-800 cursor-pointer">
<span className="flex items-center gap-1.5"><Moon className="w-3.5 h-3.5 text-indigo-500" /> Sessiz Saatler</span>
<input type="checkbox" checked={reminderSettings.quietEnabled}
onChange={(e) => setQuiet({ quietEnabled: e.target.checked })}
className="w-4 h-4 accent-indigo-600 rounded" />
</label>
{reminderSettings.quietEnabled && (
<div className="flex items-center gap-2 text-xs">
<input type="time" value={reminderSettings.quietStart}
onChange={(e) => setQuiet({ quietStart: e.target.value })}
className="bg-slate-50 border border-slate-300 rounded-lg px-2 py-1.5 flex-1" />
<span className="text-slate-400">→</span>
<input type="time" value={reminderSettings.quietEnd}
onChange={(e) => setQuiet({ quietEnd: e.target.value })}
className="bg-slate-50 border border-slate-300 rounded-lg px-2 py-1.5 flex-1" />
</div>
)}
{reminderSettings.quietEnabled && (
<p className="text-[10px] text-slate-400">
{isQuietNow() ? '🔕 Şu an sessiz saatte — tarayıcı bildirimi gösterilmez, uyarılar listeye eklenir.' : '🔔 Şu an aktif — bildirimler gösterilir.'}
</p>
)}
</div>
</div>
{/* Install PWA Application */}
+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>
);
};