Files

438 lines
17 KiB
JavaScript
Raw Permalink 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.
/* =========================================================
PulseBoard — İstatistikler sayfası
Son 7 gün, tamamlama oranı, en yoğun gün, kategori/öncelik dağılımı,
ısı haritası ve özet tablo.
========================================================= */
import store from "../store.js";
import { sutunGrafik, alanGrafik, yuzdeHalkasi, dagilimCubuklari, isiHaritasi, sparkline } from "../charts.js";
import {
kacis, bugunISO, gunKisa, kisaTarih, uzunTarih, sonNGun, yuzde,
} from "../utils.js";
/* ---------- Yardımcı kutular ---------- */
function istatistikKutusu({ etiket, deger, alt, ikon, sinif }) {
return `
<article class="kart metrik">
<div class="metrik-ust">
<span class="metrik-etiket">${kacis(etiket)}</span>
<span class="metrik-ikon ${sinif || ""}" aria-hidden="true"><svg viewBox="0 0 24 24">${ikon}</svg></span>
</div>
<div class="metrik-deger">${deger}</div>
${alt ? `<div class="metrik-alt">${alt}</div>` : ""}
</article>
`;
}
/* ---------- Bölümler ---------- */
function son7GunGrafik() {
const seri = store.sonGunSerisi(7);
const toplam = seri.reduce((t, g) => t + g.adet, 0);
const ortalama = (toplam / 7).toFixed(1);
const enIyi = seri.reduce((a, b) => (b.adet > a.adet ? b : a), seri[0]);
return `
<section class="kart" aria-labelledby="s7Baslik">
<div class="kart-baslik">
<h3 id="s7Baslik">Son 7 günde tamamlanan görevler</h3>
<span class="ipucu">Toplam ${toplam} · ortalama ${ortalama}/gün</span>
</div>
${sutunGrafik(seri, { yukseklik: 220, etiket: "Son 7 günde tamamlanan görevler" })}
<div class="istatistik-seridi" style="border-top:none;padding-bottom:0">
<span>En verimli gün: <b>${enIyi.adet > 0 ? `${gunKisa(enIyi.tarih)} (${enIyi.adet})` : "—"}</b></span>
<span>Bugün: <b>${store.gundeTamamlananGorev(bugunISO())}</b></span>
</div>
</section>
`;
}
function tamamlamaOraniBolumu() {
const gs = store.gorevIstatistigi();
const ilerleme = store.gunlukIlerleme();
return `
<section class="kart" aria-labelledby="oranBaslik">
<div class="kart-baslik">
<h3 id="oranBaslik">Görev tamamlama oranı</h3>
<span class="ipucu">Tüm zamanlar</span>
</div>
${yuzdeHalkasi(gs.oran, { altMetin: `${gs.tamamlanan}/${gs.toplam} görev` })}
<div class="dagilim" style="margin-top:var(--bosluk-4)">
<div class="dagilim-satir">
<div class="dagilim-ust">
<span class="ad"><i class="nokta" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--yesil)"></i> Tamamlanan</span>
<strong>${gs.tamamlanan}</strong>
</div>
<div class="ilerleme-bar ince"><span class="ilerleme-dolgu" style="width:${yuzde(gs.tamamlanan, gs.toplam)}%"></span></div>
</div>
<div class="dagilim-satir">
<div class="dagilim-ust">
<span class="ad"><i class="nokta" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--mavi)"></i> Açık</span>
<strong>${gs.acik}</strong>
</div>
<div class="ilerleme-bar ince"><span class="ilerleme-dolgu" style="width:${yuzde(gs.acik, gs.toplam)}%;background:var(--mavi)"></span></div>
</div>
<div class="dagilim-satir">
<div class="dagilim-ust">
<span class="ad"><i class="nokta" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--kirmizi)"></i> Gecikmiş</span>
<strong>${gs.gecikmis}</strong>
</div>
<div class="ilerleme-bar ince"><span class="ilerleme-dolgu" style="width:${yuzde(gs.gecikmis, gs.toplam)}%;background:var(--kirmizi)"></span></div>
</div>
</div>
<div class="mini-metrik" style="margin-top:var(--bosluk-4)">
<span class="mini-ikon" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/></svg></span>
<div class="mini-govde">
<strong>%${ilerleme.oran}</strong>
<span>bugünkü ilerleme (${ilerleme.tamamlanan}/${ilerleme.toplam})</span>
</div>
</div>
</section>
`;
}
function enYogunGunBolumu() {
const son30 = sonNGun(30);
const gunluk = son30.map((iso) => ({
tarih: iso,
adet: store.gundeTamamlananGorev(iso) + store.gundeTamamlananAliskanlik(iso),
}));
const enYogun = gunluk.reduce((a, b) => (b.adet > a.adet ? b : a), { tarih: null, adet: 0 });
const aktifGunler = gunluk.filter((g) => g.adet > 0).length;
// Haftanın günlerine göre dağılım (Pzt..Paz)
const gunToplamlar = [0, 0, 0, 0, 0, 0, 0];
const gunSayilari = [0, 0, 0, 0, 0, 0, 0];
gunluk.forEach((g) => {
const d = new Date(`${g.tarih}T12:00:00`);
const idx = (d.getDay() + 6) % 7; // Pazartesi = 0
gunToplamlar[idx] += g.adet;
gunSayilari[idx] += 1;
});
const enVerimliGunIdx = gunToplamlar.indexOf(Math.max(...gunToplamlar));
const gunAdlari = ["Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi", "Pazar"];
return `
<section class="kart" aria-labelledby="yogunBaslik">
<div class="kart-baslik">
<h3 id="yogunBaslik">En yoğun gün</h3>
<span class="ipucu">Son 30 gün</span>
</div>
<div class="metrik-deger" style="font-size:1.5rem">
${enYogun.tarih ? kacis(uzunTarih(new Date(`${enYogun.tarih}T12:00:00`))) : "Veri yok"}
</div>
<p style="margin-top:8px;color:var(--metin-2);font-size:.9rem">
${enYogun.adet > 0
? `Bu günde toplam <b>${enYogun.adet}</b> aktivite tamamladın (görev + alışkanlık).`
: "Henüz yeterli veri yok. Görev tamamladıkça burada öne çıkacak."}
</p>
<div class="mini-metrik">
<span class="mini-ikon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 20V10M12 20V4M19 20v-6"/></svg></span>
<div class="mini-govde">
<strong>${aktifGunler}/30</strong>
<span>aktif gün (son 30 günde)</span>
</div>
</div>
<div class="mini-metrik">
<span class="mini-ikon" style="color:var(--mor);background:rgba(168,85,247,.12)" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M12 3v3M12 18v3M3 12h3M18 12h3"/><circle cx="12" cy="12" r="4"/></svg>
</span>
<div class="mini-govde">
<strong>${enVerimliGunIdx >= 0 && gunToplamlar[enVerimliGunIdx] > 0 ? kacis(gunAdlari[enVerimliGunIdx]) : "—"}</strong>
<span>haftanın en verimli günü</span>
</div>
</div>
</section>
`;
}
function aktifAliskanlikBolumu() {
const { aliskanliklar } = store.al();
const bugun = bugunISO();
const son7 = sonNGun(7);
const aktif = aliskanliklar.filter((a) => son7.some((g) => a.yapilanGunler.includes(g)));
const toplamIsaret = aliskanliklar.reduce((t, a) => t + a.yapilanGunler.length, 0);
const bugunIsaret = aliskanliklar.filter((a) => a.yapilanGunler.includes(bugun)).length;
const sirali = [...aliskanliklar]
.map((a) => ({
ad: a.ad,
emoji: a.emoji,
renk: a.renk,
seri: store.seri(a.id),
rekor: store.enUzunSeri(a.id),
toplam: a.yapilanGunler.length,
son7: son7.filter((g) => a.yapilanGunler.includes(g)).length,
}))
.sort((a, b) => b.son7 - a.son7 || b.seri - a.seri);
return `
<section class="kart" aria-labelledby="alisStatBaslik">
<div class="kart-baslik">
<h3 id="alisStatBaslik">Alışkanlık performansı</h3>
<span class="ipucu">${aktif.length}/${aliskanliklar.length} aktif (son 7 gün)</span>
</div>
${aliskanliklar.length === 0
? `<div class="bos-durum" style="padding:var(--bosluk-5) var(--bosluk-3)">
<p>Henüz alışkanlık eklenmedi. <a href="#/aliskanliklar" style="color:var(--teal);font-weight:600">Alışkanlık ekle →</a></p>
</div>`
: `
<div class="izgara izgara-3" style="margin-bottom:var(--bosluk-5)">
<div class="metrik" style="padding:0">
<span class="metrik-etiket">Aktif alışkanlık</span>
<span class="metrik-deger" style="font-size:1.5rem">${aktif.length}</span>
</div>
<div class="metrik" style="padding:0">
<span class="metrik-etiket">Bugün işaretli</span>
<span class="metrik-deger" style="font-size:1.5rem">${bugunIsaret}/${aliskanliklar.length}</span>
</div>
<div class="metrik" style="padding:0">
<span class="metrik-etiket">Toplam kayıt</span>
<span class="metrik-deger" style="font-size:1.5rem">${toplamIsaret}</span>
</div>
</div>
<div class="tablo-sarmal">
<table class="tablo">
<caption class="gorunmez-metin">Alışkanlık başına seri ve kayıt sayıları</caption>
<thead>
<tr>
<th scope="col">Alışkanlık</th>
<th scope="col">Son 7 gün</th>
<th scope="col">Aktif seri</th>
<th scope="col">Rekor</th>
<th scope="col">Toplam</th>
</tr>
</thead>
<tbody>
${sirali.map((a) => `
<tr>
<td>${kacis(a.emoji)} ${kacis(a.ad)}</td>
<td class="sayi">
<span style="display:inline-flex;align-items:center;gap:8px">
<span class="ilerleme-bar ince" style="width:64px;display:inline-block;vertical-align:middle">
<span class="ilerleme-dolgu" style="width:${Math.round((a.son7 / 7) * 100)}%;background:${kacis(a.renk)}"></span>
</span>
${a.son7}/7
</span>
</td>
<td class="sayi">${a.seri > 0 ? `🔥 ${a.seri}` : "0"}</td>
<td class="sayi">${a.rekor}</td>
<td class="sayi">${a.toplam}</td>
</tr>`).join("")}
</tbody>
</table>
</div>
`}
</section>
`;
}
function dagilimBolumu() {
const ist = store.istatistikOzeti();
const oncelikListesi = ist.oncelikDagilim.map((o) => ({
ad: o.id === "dusuk" ? "Düşük" : o.id === "orta" ? "Orta" : "Yüksek",
adet: o.adet,
renk: o.id === "dusuk" ? "#38bdf8" : o.id === "orta" ? "#fbbf24" : "#fb923c",
}));
return `
<section class="kart" aria-labelledby="dagilimBaslik">
<div class="kart-baslik">
<h3 id="dagilimBaslik">Öncelik dağılımı</h3>
<span class="ipucu">Tüm görevler</span>
</div>
${dagilimCubuklari(oncelikListesi, { bosMesaj: "Henüz görev eklenmedi." })}
</section>
<section class="kart" aria-labelledby="kategoriBaslik">
<div class="kart-baslik">
<h3 id="kategoriBaslik">Kategori dağılımı</h3>
<span class="ipucu">${ist.kategoriDagilim.length} kategori</span>
</div>
${dagilimCubuklari(ist.kategoriDagilim, { bosMesaj: "Kategorilere göre veri oluşmadı." })}
</section>
`;
}
function isiHaritasiBolumu() {
const gunler = store.isiHaritasi();
return `
<section class="kart" aria-labelledby="isiBaslik">
<div class="kart-baslik">
<h3 id="isiBaslik">Aktivite haritası</h3>
<span class="ipucu">Son 12 hafta · görev + alışkanlık</span>
</div>
${isiHaritasi(gunler)}
</section>
`;
}
function ozetTablosu() {
const ist = store.istatistikOzeti();
const gs = store.gorevIstatistigi();
const bugun = bugunISO();
const satirlar = [
["Toplam görev", gs.toplam],
["Tamamlanan görev", gs.tamamlanan],
["Açık görev", gs.acik],
["Gecikmiş görev", gs.gecikmis],
["Bugün teslim edilecek", gs.bugunTeslim],
["Genel tamamlama oranı", `%${gs.oran}`],
["Bugün tamamlanan", store.gundeTamamlananGorev(bugun)],
["Son 7 günde tamamlanan", ist.toplamTamam],
["Günlük ortalama (7 gün)", ist.gunlukOrtalama],
["Toplam alışkanlık", ist.toplamAliskanlik],
["Aktif alışkanlık (7 gün)", ist.aktifAliskanlik],
];
return `
<section class="kart" aria-labelledby="tabloBaslik">
<div class="kart-baslik">
<h3 id="tabloBaslik">Özet tablo</h3>
<span class="ipucu">${kacis(uzunTarih())}</span>
</div>
<div class="tablo-sarmal">
<table class="tablo">
<caption class="gorunmez-metin">PulseBoard genel istatistik özeti</caption>
<tbody>
${satirlar.map(([ad, deger]) => `
<tr><td>${kacis(ad)}</td><td class="sayi">${kacis(String(deger))}</td></tr>`).join("")}
</tbody>
</table>
</div>
</section>
`;
}
/* ---------- Boş durum ---------- */
function istatistikBosDurum() {
return `
<div class="gorunum-girisi">
<div class="bolum-basligi">
<div>
<h2>İstatistikler</h2>
<p>İlerlemeni sayılarla takip et.</p>
</div>
</div>
<div class="kart">
<div class="bos-durum">
<span class="bos-ikon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 20V10M12 20V4M19 20v-6"/></svg></span>
<h3>Henüz istatistik oluşturacak veri yok</h3>
<p>Görev tamamla ve alışkanlıklarını işaretle — bu sayfa otomatik olarak dolmaya başlar.</p>
<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-top:var(--bosluk-2)">
<button class="btn btn-ana" type="button" data-eylem="yeni-gorev">Görev ekle</button>
<button class="btn btn-sade" type="button" data-eylem="ornek-veri">Örnek veriyle doldur</button>
</div>
</div>
</div>
</div>
`;
}
/* ---------- Render ---------- */
export function render() {
if (store.bosMu()) return istatistikBosDurum();
const ist = store.istatistikOzeti();
const gs = store.gorevIstatistigi();
const gunler14 = store.sonGunSerisi(14).map((g) => g.adet);
return `
<div class="gorunum-girisi">
<div class="bolum-basligi">
<div>
<h2>İstatistikler</h2>
<p>Son 7 gün, tamamlama oranı, en yoğun gün ve kategori dağılımı.</p>
</div>
<span class="chip chip-notr">${kacis(uzunTarih())}</span>
</div>
<div class="izgara izgara-4 kompakt-mobil" style="margin-bottom:var(--bosluk-5)">
${istatistikKutusu({
etiket: "Son 7 günde tamamlanan",
deger: `${ist.toplamTamam}`,
alt: `<span>günlük ortalama <b>${ist.gunlukOrtalama}</b></span>`,
ikon: '<path d="M20 6 9 17l-5-5"/>',
})}
${istatistikKutusu({
etiket: "Tamamlama oranı",
deger: `%${gs.oran}`,
alt: `<span>${gs.tamamlanan}/${gs.toplam} görev</span>`,
ikon: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
sinif: "mavi",
})}
${istatistikKutusu({
etiket: "En yoğun gün",
deger: ist.enYogun.adet > 0 ? `${ist.enYogun.adet}` : "—",
alt: `<span>${ist.enYogun.tarih && ist.enYogun.adet > 0 ? kacis(kisaTarih(ist.enYogun.tarih)) : "veri yok"}</span>`,
ikon: '<path d="M13 2 4.5 13H11l-1 9 8.5-11H12l1-9Z"/>',
sinif: "mor",
})}
${istatistikKutusu({
etiket: "Aktif alışkanlık",
deger: `${ist.aktifAliskanlik}<small>/${ist.toplamAliskanlik}</small>`,
alt: `<span>son 7 günde işaretlenen</span>`,
ikon: '<path d="M12 21s-7-4.4-7-9.6A4.4 4.4 0 0 1 12 8a4.4 4.4 0 0 1 7 3.4C19 16.6 12 21 12 21Z"/>',
sinif: "sari",
})}
</div>
<div class="izgara izgara-2-1" style="margin-bottom:var(--bosluk-5)">
${son7GunGrafik()}
${tamamlamaOraniBolumu()}
</div>
<div class="izgara izgara-2-1" style="margin-bottom:var(--bosluk-5)">
${enYogunGunBolumu()}
<section class="kart" aria-labelledby="egilim14Baslik">
<div class="kart-baslik">
<h3 id="egilim14Baslik">14 günlük eğilim</h3>
<span class="ipucu">Tamamlanan görev</span>
</div>
${alanGrafik(store.sonGunSerisi(14), { yukseklik: 152, etiket: "14 günlük tamamlama eğilimi" })}
<div style="margin-top:var(--bosluk-4)">${sparkline(gunler14, { renk: "#a855f7" })}</div>
</section>
</div>
<div class="izgara izgara-2" style="margin-bottom:var(--bosluk-5)">
${dagilimBolumu()}
</div>
${isiHaritasiBolumu()}
<div style="margin-top:var(--bosluk-5)">
${ozetTablosu()}
</div>
</div>
`;
}
/* ---------- Olaylar ---------- */
export function bagla(kok, yenidenCiz) {
kok.addEventListener("click", (olay) => {
const hedef = olay.target.closest("[data-eylem]");
if (!hedef || !kok.contains(hedef)) return;
const eylem = hedef.dataset.eylem;
if (eylem === "yeni-gorev") {
// app.js'te global hızlı ekleme ile aynı davranış
document.getElementById("hizliEkleBtn")?.click();
} else if (eylem === "ornek-veri") {
store.ornekVeriYukle();
yenidenCiz?.();
}
});
}
export default { render, bagla, istatistikKutusu };