feat: tüm mock veriyi kaldır + Resend e-posta entegrasyonu
- mockTenders.js silindi; referans listeleri (sektör/il/kurum) constants.js'e taşındı - AppContext: tenders/liveStats/competitorWatchlist/vaultDocuments/notifications/ savedFilters/apiKeys/scrapedCapabilities → gerçek/boş; sahte Math.random online sayacı kaldırıldı; competitor & belge kasası artık localStorage - LiveStatsBar: gerçek alanlar (canlı havuz/yüklü/bugün/kalem/takibimdeki) - AnalyticsDashboard: KPI'lar (katılım/kazanma oranı/kazanılan bedel) + aylık hacim + sektörel dağılım gerçek companyStats/monthlyStats/sectorStats'tan; ciro hedefi kullanıcı tarafından belirlenir; veri yoksa dürüst boş durum - ContractSearch & Rakip Radar: gerçek firmStats - analytics.js: companyStats/monthlyStats/parseDateTR eklendi - Resend: server/email.mjs + /api/notify/email/config + /api/notify/email/test (auth'lu, kullanıcının kendi adresine); PwaSettings gerçek e-posta test kartı
This commit is contained in:
@@ -107,6 +107,63 @@ export function sectorStats(tenders) {
|
||||
return [...map.entries()].map(([sektor, count]) => ({ sektor, count })).sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
// "10.08.2026" → Date
|
||||
export function parseDateTR(s) {
|
||||
if (!s) return null;
|
||||
const m = String(s).match(/(\d{1,2})[.\/](\d{1,2})[.\/](\d{4})/);
|
||||
if (!m) return null;
|
||||
return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||
}
|
||||
|
||||
// --- Firmanın kendi performansı (kullanıcının kanban/takip durumundan) ---
|
||||
const BID_STATES = ['teklif_verildi', 'kazanildi', 'kaybedildi'];
|
||||
export function companyStats(tenders) {
|
||||
const won = tenders.filter((t) => t.kanbanStatus === 'kazanildi');
|
||||
const lost = tenders.filter((t) => t.kanbanStatus === 'kaybedildi');
|
||||
const bid = tenders.filter((t) => BID_STATES.includes(t.kanbanStatus));
|
||||
const wonTotal = won.reduce(
|
||||
(s, t) => s + (parseTL(t.sozlesmeBedeli) ?? parseTL(t.yaklasikMaliyet) ?? 0),
|
||||
0
|
||||
);
|
||||
const resulted = won.length + lost.length;
|
||||
const kanban = {};
|
||||
for (const st of ['incelenecek', 'inceleniyor', 'teklif_hazirlaniyor', 'teklif_verildi', 'kazanildi', 'kaybedildi']) {
|
||||
kanban[st] = tenders.filter((t) => (t.kanbanStatus || '') === st).length;
|
||||
}
|
||||
return {
|
||||
participated: bid.length,
|
||||
won: won.length,
|
||||
lost: lost.length,
|
||||
resulted,
|
||||
winRate: resulted ? Math.round((won.length / resulted) * 100) : 0,
|
||||
wonTotal,
|
||||
tracked: tenders.filter((t) => t.takipte).length,
|
||||
reviewed: tenders.filter((t) => t.incelendi || (t.kanbanStatus && t.kanbanStatus !== 'incelenecek')).length,
|
||||
contracts: won.length,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Aylık kazanım / katılım hacmi (son 7 ay) ---
|
||||
const AY_KISA = ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Eki', 'Kas', 'Ara'];
|
||||
export function monthlyStats(tenders) {
|
||||
const map = new Map();
|
||||
for (const t of tenders) {
|
||||
if (!BID_STATES.includes(t.kanbanStatus)) continue;
|
||||
const d = parseDateTR(t.tarih);
|
||||
if (!d) continue;
|
||||
const key = `${d.getFullYear()}-${String(d.getMonth()).padStart(2, '0')}`;
|
||||
const cur = map.get(key) || { y: d.getFullYear(), m: d.getMonth(), kazanim: 0, katilim: 0 };
|
||||
const val = parseTL(t.sozlesmeBedeli) ?? parseTL(t.yaklasikMaliyet) ?? 0;
|
||||
cur.katilim += val;
|
||||
if (t.kanbanStatus === 'kazanildi') cur.kazanim += val;
|
||||
map.set(key, cur);
|
||||
}
|
||||
return [...map.values()]
|
||||
.sort((a, b) => a.y - b.y || a.m - b.m)
|
||||
.slice(-7)
|
||||
.map((x) => ({ month: AY_KISA[x.m], kazanim: x.kazanim, katilim: x.katilim }));
|
||||
}
|
||||
|
||||
// --- Alım türü dağılımı ---
|
||||
export function alimTuruStats(tenders) {
|
||||
const map = new Map();
|
||||
|
||||
Reference in New Issue
Block a user