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:
2026-08-28 08:49:10 +03:00
parent 7521f798f6
commit 3e129c2988
14 changed files with 497 additions and 555 deletions
+57
View File
@@ -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();