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