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

195 lines
11 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, useEffect } from 'react';
import { useApp } from '../../context/AppContext';
import { BarChart3, TrendingUp, Trophy, Target, Award, ArrowUpRight, Pencil } from 'lucide-react';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, PieChart, Pie, Cell } from 'recharts';
import { TurkeyHeatMap } from './TurkeyHeatMap';
import { companyStats, monthlyStats, sectorStats, formatTL } from '../../services/analytics';
const PIE_COLORS = ['#2563eb', '#059669', '#7c3aed', '#d97706', '#dc2626', '#0891b2', '#db2777', '#65a30d'];
export const AnalyticsDashboard = () => {
const { tenders, userProfile, authUser } = useApp();
const cs = companyStats(tenders);
const monthlyData = monthlyStats(tenders);
const sektorler = sectorStats(tenders);
const pieData = sektorler.slice(0, 6).map((s, i) => ({ name: s.sektor, value: s.count, color: PIE_COLORS[i % PIE_COLORS.length] }));
const pieToplam = pieData.reduce((s, p) => s + p.value, 0);
// Ciro hedefi hesap bazında tutulur; ortak anahtar hesaplar arası veri sızdırır.
const hedefAnahtari = authUser?.email ? `ihaleist_ciro_hedefi:${encodeURIComponent(authUser.email.toLowerCase())}` : null;
const [hedef, setHedef] = useState(0);
const [hedefYuklenenAnahtar, setHedefYuklenenAnahtar] = useState(null);
const [editing, setEditing] = useState(false);
useEffect(() => {
if (!hedefAnahtari) { setHedef(0); setHedefYuklenenAnahtar(null); return; }
try { setHedef(Number(localStorage.getItem(hedefAnahtari) || 0) || 0); } catch { setHedef(0); }
setHedefYuklenenAnahtar(hedefAnahtari);
}, [hedefAnahtari]);
useEffect(() => {
if (!hedefAnahtari || hedefYuklenenAnahtar !== hedefAnahtari) return;
try { localStorage.setItem(hedefAnahtari, String(hedef)); } catch { /* özel tarama/kota */ }
}, [hedef, hedefAnahtari, hedefYuklenenAnahtar]);
const hedefYuzde = hedef > 0 ? Math.min(Math.round((cs.wonTotal / hedef) * 100), 100) : 0;
return (
<div className="space-y-6 animate-fade-in">
{/* Header Banner */}
<div className="bg-linear-to-r from-slate-900 via-purple-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-purple-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-purple-600 text-white flex items-center justify-center font-black shadow-md">
<BarChart3 className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Analitik Dashboard & Fiyat Isı Haritası</h2>
<p className="text-xs text-purple-200 mt-0.5">{userProfile.company} — kanban süreçlerinizden hesaplanan gerçek kazanma oranı ve il bazlı ihale yoğunluğu.</p>
</div>
</div>
</div>
{/* KPI Cards & Ciro Target — gerçek (kanban/takip verisinden) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Katıldığımız İhaleler</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-slate-900">{cs.participated} İhale</span>
<div className="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center">
<Trophy className="w-4 h-4" />
</div>
</div>
<span className="text-[11px] text-slate-400">Teklif verilen + sonuçlanan</span>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">İhale Kazanma Oranımız</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-emerald-600">%{cs.winRate}</span>
{cs.resulted > 0 && (
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full flex items-center gap-0.5">
<ArrowUpRight className="w-3 h-3" /> {cs.won}/{cs.resulted}
</span>
)}
</div>
<span className="text-[11px] text-slate-400">{cs.resulted > 0 ? `${cs.resulted} sonuçlanan ihaleden` : 'Henüz sonuçlanan ihale yok'}</span>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Toplam Kazanılan Bedel</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-indigo-900">{formatTL(cs.wonTotal)}</span>
<div className="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center">
<Award className="w-4 h-4" />
</div>
</div>
<span className="text-[11px] text-slate-400">{cs.contracts} sözleşme</span>
</div>
{/* Ciro Hedefi — kullanıcı belirler */}
<div className="bg-linear-to-br from-blue-900 to-indigo-900 text-white p-5 rounded-2xl shadow-xs space-y-2">
<div className="flex justify-between items-center text-xs font-bold">
<span className="text-blue-200 uppercase tracking-wider flex items-center gap-1">
<Target className="w-3.5 h-3.5 text-amber-300" /> Ciro Hedefi
</span>
<button onClick={() => setEditing((e) => !e)} className="text-blue-200 hover:text-white" title="Hedefi düzenle">
{hedef > 0 ? <span className="text-amber-300 font-black">%{hedefYuzde}</span> : <Pencil className="w-3.5 h-3.5" />}
</button>
</div>
{editing || hedef === 0 ? (
<div className="flex items-center gap-1.5 pt-1">
<input
type="number"
value={hedef || ''}
onChange={(e) => setHedef(Number(e.target.value) || 0)}
placeholder="Hedef bedel (₺)"
className="w-full bg-blue-950/60 border border-blue-700 rounded-lg px-2 py-1.5 text-xs text-white placeholder:text-blue-300"
/>
<button onClick={() => setEditing(false)} className="bg-amber-400 text-blue-950 font-black text-xs px-2.5 py-1.5 rounded-lg">Kaydet</button>
</div>
) : (
<>
<span className="text-lg font-black text-white block">{formatTL(cs.wonTotal)} / {formatTL(hedef)}</span>
<div className="w-full bg-blue-950 rounded-full h-2 overflow-hidden border border-blue-700">
<div className="bg-amber-400 h-2 rounded-full transition-all" style={{ width: `${hedefYuzde}%` }} />
</div>
</>
)}
</div>
</div>
{/* Recharts Row — gerçek veriden */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Monthly Revenue Chart */}
<div className="lg:col-span-2 bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-blue-600" /> Aylık İhale Kazanım ve Teklif Hacmi (TL)
</h3>
<div className="h-64 w-full">
{monthlyData.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-center border-2 border-dashed border-slate-200 rounded-xl">
<p className="text-sm font-bold text-slate-400">Henüz teklif/sonuç verisi yok</p>
<p className="text-xs text-slate-400 mt-1 max-w-xs">Kanban'da ihaleleri "teklif verildi / kazanıldı / kaybedildi" durumuna aldıkça aylık hacim grafiği oluşur.</p>
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={60} tickFormatter={(val) => `₺${Math.round(val / 1000)}k`} />
<Tooltip formatter={(value) => [`₺${value.toLocaleString('tr-TR')}`, '']} />
<Bar dataKey="katilim" fill="#94a3b8" name="Toplam Verilen Teklif" radius={[4, 4, 0, 0]} />
<Bar dataKey="kazanim" fill="#2563eb" name="Kazanılan İhale Hacmi" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
)}
</div>
</div>
{/* Sector Distribution Pie */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<h3 className="font-extrabold text-sm text-slate-900">Sektörel İhale Dağılımı</h3>
{pieData.length === 0 ? (
<div className="h-48 flex items-center justify-center text-center border-2 border-dashed border-slate-200 rounded-xl">
<p className="text-xs text-slate-400 max-w-[180px]">Sektör dağılımı, ihale verisi yüklendikçe hesaplanır.</p>
</div>
) : (
<>
<div className="h-48 w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" innerRadius={40} outerRadius={70} paddingAngle={4}>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip formatter={(value) => [`${value} ihale`, 'Adet']} />
</PieChart>
</ResponsiveContainer>
</div>
<div className="space-y-1.5 pt-2">
{pieData.map((item) => (
<div key={item.name} className="flex justify-between items-center text-xs font-semibold">
<span className="flex items-center gap-2 min-w-0">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: item.color }}></span>
<span className="truncate">{item.name}</span>
</span>
<span className="font-bold text-slate-900 shrink-0">%{pieToplam ? Math.round((item.value / pieToplam) * 100) : 0}</span>
</div>
))}
</div>
</>
)}
</div>
</div>
{/* İl bazlı gerçek yoğunluk haritası */}
<TurkeyHeatMap />
</div>
);
};