feat: dashboard substansial — harga close per kartu, konteks pasar, grafik berformat Rp + periode
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-09-15 20:55:38 +07:00
parent dab3a9d394
commit 793e0724ed
4 changed files with 109 additions and 15 deletions
+44 -11
View File
@@ -1,7 +1,7 @@
import { createResource, createSignal, For, Show } from "solid-js";
import { A, useNavigate } from "@solidjs/router";
import { api, fmtIDR } from "../lib/api";
import { verdictFor, heroSummary } from "../lib/awam";
import { api } from "../lib/api";
import { verdictFor, heroSummary, konteksPasar, fmtRp, fmtHarga } from "../lib/awam";
import { Term, VerdictBadge, AlasanBar, IstilahStrip } from "../components/Awam";
import { Badge } from "../components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
@@ -27,10 +27,32 @@ export default function Dashboard() {
return p ? heroSummary(p) : [];
};
const sorotan = () => (screen()?.rows || []).map((r) => ({ row: r, ...verdictFor(r) }));
const closeMap = () => Object.fromEntries((flow()?.closes || []).map((c) => [c.ticker, c]));
const chartData = () => ({
labels: (foreign()?.dates || []) as string[],
datasets: [{ label: `${chartTiker()} — uang asing harian`, data: (foreign()?.nets || []) as number[] }],
datasets: [{
label: `${chartTiker()} — uang asing harian (${fmtRp(0).slice(0, 2)}7700`,
data: (foreign()?.nets || []) as number[],
borderColor: "#3b82f6",
backgroundColor: "rgba(59,130,246,.15)",
fill: true,
tension: 0.25,
}],
});
const chartOpts = () => ({
responsive: true,
plugins: {
tooltip: { callbacks: { label: (c: { parsed: { y: number } }) => ` ${fmtRp(c.parsed.y)}` } },
legend: { display: false },
},
scales: {
y: { ticks: { callback: (v: number | string) => fmtRp(Number(v)), maxTicksLimit: 5 } },
},
});
const periode = () => {
const f = foreign();
return f?.start && f?.end ? `${f.start} → ${f.end}` : "";
};
return (
<div class="space-y-6">
@@ -48,6 +70,9 @@ export default function Dashboard() {
</ul>
</Show>
</Show>
<Show when={sorotan().length}>
<p class="mt-3 border-t pt-3 text-sm text-muted-foreground">🧭 {konteksPasar(sorotan(), flow()?.foreign_net_total)}</p>
</Show>
<div class="mt-3"><IstilahStrip /></div>
</CardContent>
</Card>
@@ -55,22 +80,29 @@ export default function Dashboard() {
{/* SOROTAN SAHAM */}
<div>
<h2 class="mb-3 text-lg font-semibold">Saham sorotan hari ini</h2>
<Show when={!screen.loading} fallback={<div class="grid gap-4 md:grid-cols-2"><Skeleton class="h-40" /><Skeleton class="h-40" /></div>}>
<Show when={!screen.loading} fallback={<div class="grid gap-4 md:grid-cols-2"><Skeleton class="h-44" /><Skeleton class="h-44" /></div>}>
<Show when={sorotan().length} fallback={<p class="text-sm text-muted-foreground">Belum ada data sorotan.</p>}>
<div class="grid gap-4 md:grid-cols-2">
<For each={sorotan()}>{(s) => {
const b = (s.row.breakdown || {}) as Record<string, unknown>;
const cx = closeMap()[s.row.symbol];
return (
<Card>
<Card class={s.verdict === "Dilirik" ? "border-l-4 border-l-emerald-500" : s.verdict === "Dilepas" ? "border-l-4 border-l-red-500" : ""}>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-xl font-bold">{s.row.symbol}</CardTitle>
<CardTitle class="text-xl font-bold">
{s.row.symbol}
<Show when={cx?.close}><span class="ml-2 font-mono text-sm font-normal text-muted-foreground">{fmtHarga(cx.close)}</span></Show>
</CardTitle>
<VerdictBadge verdict={s.verdict} />
</CardHeader>
<CardContent class="space-y-3">
<p class="text-sm">Karena: <strong>{s.alasan}</strong>.</p>
<p class="text-sm">
Harga terakhir <strong class="font-mono">{cx?.close ? fmtHarga(cx.close) : "—"}</strong>
{" "}— {s.alasan}.
</p>
<AlasanBar asing={Number(b.foreign ?? 0)} broker={Number(b.broker_score ?? b.broker ?? 0)} />
<div class="flex gap-2">
<Button size="sm" onClick={() => navigate(`/report?t=${s.row.symbol}`)}>Kenapa? Jelaskan</Button>
<Button size="sm" onClick={() => navigate(`/report/${s.row.symbol}`)}>Kenapa? Jelaskan</Button>
<Button size="sm" variant="outline"><A href={`/screener`}>Bandingkan</A></Button>
</div>
</CardContent>
@@ -95,7 +127,8 @@ export default function Dashboard() {
}</For>
</div>
<Show when={(foreign()?.dates || []).length} fallback={<p class="text-sm text-muted-foreground">Belum ada data grafik.</p>}>
<Line data={chartData()} options={{ responsive: true }} />
<Line data={chartData()} options={chartOpts() as never} />
<p class="mt-1 text-xs text-muted-foreground">Periode {periode()} · sumber: Sectors API (lihat sitasi report).</p>
<Show when={foreign()?.reversal}><p class="mt-2 text-sm font-medium">⚠️ Arahnya baru berbalik — perhatikan beberapa hari ke depan.</p></Show>
</Show>
</CardContent>
@@ -115,10 +148,10 @@ export default function Dashboard() {
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Yang paling diborong</CardTitle><CardDescription>Total <Term kata="net" /> beli broker hari ini: <strong>{flow() ? fmtIDR(flow()!.foreign_net_total) : "…"}</strong></CardDescription></CardHeader>
<CardHeader><CardTitle>Yang paling diborong</CardTitle><CardDescription>Total <Term kata="net" /> beli broker 5 hari: <strong>{flow() ? fmtRp(flow()!.foreign_net_total) : "…"}</strong></CardDescription></CardHeader>
<CardContent>
<ul class="space-y-1 text-sm">
<For each={flow()?.top_accumulation || []}>{(t) => <li class="flex justify-between"><A class="font-medium underline-offset-2 hover:underline" href={`/report?t=${t.ticker}`}>{t.ticker}</A><span class="font-mono">{fmtIDR(t.net_sum)} · {t.brokers} broker</span></li>}</For>
<For each={flow()?.top_accumulation || []}>{(t) => <li class="flex justify-between"><A class="font-medium underline-offset-2 hover:underline" href={`/report/${t.ticker}`}>{t.ticker}</A><span class="font-mono">{fmtRp(t.net_sum)} · {t.brokers} broker</span></li>}</For>
</ul>
<div class="mt-2"><Badge variant="outline">Angka detail untuk yang penasaran — keputusan ada di kartu sorotan ☝️</Badge></div>
</CardContent>