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,118 @@
|
||||
/**
|
||||
* Gerçek ihale verisinden (context'teki tenders dizisi) hesaplanan analizler.
|
||||
* Sabit mock yok — ne yüklüyse ondan üretir. Veri arttıkça (DT worker, sonuç
|
||||
* ilanları) analizler otomatik zenginleşir.
|
||||
*/
|
||||
|
||||
// "22.800,00 TRY" / "1.234.567,89 TL" gibi metinleri sayıya çevir
|
||||
export function parseTL(v) {
|
||||
if (v == null) return null;
|
||||
if (typeof v === 'number') return isFinite(v) ? v : null;
|
||||
const s = String(v).replace(/[^\d.,]/g, '');
|
||||
if (!s) return null;
|
||||
const norm = s.replace(/\./g, '').replace(',', '.');
|
||||
const n = parseFloat(norm);
|
||||
return isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
export function formatTL(n) {
|
||||
if (n == null || !isFinite(n)) return '—';
|
||||
return n.toLocaleString('tr-TR', { maximumFractionDigits: 0 }) + ' ₺';
|
||||
}
|
||||
|
||||
// İl plaka koduna göre density için normalize edilmiş il adı
|
||||
export function normProvince(name) {
|
||||
return (name || '').toLocaleUpperCase('tr-TR').trim();
|
||||
}
|
||||
|
||||
// --- Kazanan firma / rakip analizi ---
|
||||
export function firmStats(tenders) {
|
||||
const map = new Map();
|
||||
let awardCount = 0;
|
||||
for (const t of tenders) {
|
||||
if (!t.kazananFirma) continue;
|
||||
awardCount++;
|
||||
const name = t.kazananFirma.trim();
|
||||
const val = parseTL(t.sozlesmeBedeli);
|
||||
const cur = map.get(name) || { name, wins: 0, total: 0, sectors: new Set(), cities: new Set() };
|
||||
cur.wins++;
|
||||
if (val != null) cur.total += val;
|
||||
if (t.sektor) cur.sectors.add(t.sektor);
|
||||
if (t.sehir) cur.cities.add(t.sehir);
|
||||
map.set(name, cur);
|
||||
}
|
||||
const firms = [...map.values()]
|
||||
.map((f) => ({ ...f, sectors: [...f.sectors], cities: [...f.cities] }))
|
||||
.sort((a, b) => b.wins - a.wins || b.total - a.total);
|
||||
const totalMarket = firms.reduce((s, f) => s + f.total, 0);
|
||||
return {
|
||||
firms: firms.map((f) => ({ ...f, share: totalMarket ? Math.round((f.total / totalMarket) * 100) : 0 })),
|
||||
awardCount,
|
||||
totalMarket,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Kalem bazlı fiyat analizi ---
|
||||
export function itemStats(tenders) {
|
||||
const map = new Map();
|
||||
let itemCount = 0;
|
||||
for (const t of tenders) {
|
||||
for (const it of t.items || []) {
|
||||
const bf = parseTL(it.birimFiyat);
|
||||
if (bf == null) continue;
|
||||
itemCount++;
|
||||
const key = (it.ad || 'Diğer').trim().toLocaleUpperCase('tr-TR');
|
||||
const cur = map.get(key) || { ad: it.ad, birim: it.birim, count: 0, prices: [] };
|
||||
cur.count++;
|
||||
cur.prices.push(bf);
|
||||
map.set(key, cur);
|
||||
}
|
||||
}
|
||||
const items = [...map.values()]
|
||||
.map((i) => {
|
||||
const sorted = [...i.prices].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
return {
|
||||
ad: i.ad,
|
||||
birim: i.birim,
|
||||
count: i.count,
|
||||
min: sorted[0],
|
||||
max: sorted[sorted.length - 1],
|
||||
median: sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2,
|
||||
avg: i.prices.reduce((s, p) => s + p, 0) / i.prices.length,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.count - a.count);
|
||||
return { items, itemCount, uniqueItems: items.length };
|
||||
}
|
||||
|
||||
// --- İl bazlı yoğunluk (harita için) ---
|
||||
export function provinceDensity(tenders) {
|
||||
const map = new Map();
|
||||
for (const t of tenders) {
|
||||
const p = normProvince(t.sehir);
|
||||
if (!p) continue;
|
||||
map.set(p, (map.get(p) || 0) + 1);
|
||||
}
|
||||
return map; // İL_ADI(upper) -> sayı
|
||||
}
|
||||
|
||||
// --- Sektör dağılımı ---
|
||||
export function sectorStats(tenders) {
|
||||
const map = new Map();
|
||||
for (const t of tenders) {
|
||||
const s = t.sektor || 'Genel';
|
||||
map.set(s, (map.get(s) || 0) + 1);
|
||||
}
|
||||
return [...map.entries()].map(([sektor, count]) => ({ sektor, count })).sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
// --- Alım türü dağılımı ---
|
||||
export function alimTuruStats(tenders) {
|
||||
const map = new Map();
|
||||
for (const t of tenders) {
|
||||
const a = t.alimTuru || 'Diğer';
|
||||
map.set(a, (map.get(a) || 0) + 1);
|
||||
}
|
||||
return [...map.entries()].map(([tur, count]) => ({ tur, count })).sort((a, b) => b.count - a.count);
|
||||
}
|
||||
Reference in New Issue
Block a user