From 390133663a5a3d771fd78d5dc3f0353a575c029e Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 15 Sep 2026 19:06:35 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20dashboard=20awam-first=20=E2=80=94=20he?= =?UTF-8?q?ro=20ringkasan,=20kartu=20verdict,=20tooltip=20istilah?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/Awam.tsx | 54 ++++++++++++ web/src/lib/awam.ts | 70 +++++++++++++++ web/src/pages/Dashboard.tsx | 167 +++++++++++++++++++++++------------- 3 files changed, 231 insertions(+), 60 deletions(-) create mode 100644 web/src/components/Awam.tsx create mode 100644 web/src/lib/awam.ts diff --git a/web/src/components/Awam.tsx b/web/src/components/Awam.tsx new file mode 100644 index 0000000..5abcc43 --- /dev/null +++ b/web/src/components/Awam.tsx @@ -0,0 +1,54 @@ +import { For, Show } from "solid-js"; +import { Badge } from "./ui/badge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { ISTILAH, type Verdict } from "../lib/awam"; + +// Kata teknis bergaris-titik + tooltip penjelasan santai. +export function Term(props: { kata: string }) { + const key = props.kata.toLowerCase(); + const arti = ISTILAH[key]; + if (!arti) return {props.kata}; + return ( + + {props.kata} + {arti} + + ); +} + +export function VerdictBadge(props: { verdict: Verdict }) { + const warna: Record = { + "Dilirik": "default", + "Dilepas": "error", + "Netral": "secondary", + }; + return {props.verdict}; +} + +// Bar alasan: seberapa kuat sinyal asing vs broker (0-100). +export function AlasanBar(props: { asing: number; broker: number }) { + const total = Math.abs(props.asing) + Math.abs(props.broker) || 1; + const pAsing = Math.round((Math.abs(props.asing) / total) * 100); + return ( +
+
+
+
+
+
+ ■ Asing {props.asing >= 0 ? "masuk" : "keluar"} + ■ Broker {props.broker >= 0 ? "borong" : "jualan"} +
+
+ ); +} + +export function IstilahStrip() { + const kunci = ["akumulasi", "foreign flow", "broker", "net", "briefing", "alert"]; + return ( +

+ Istilah asing? Arahkan kursor:{" "} + {(k, i) => <> 0}>, } +

+ ); +} diff --git a/web/src/lib/awam.ts b/web/src/lib/awam.ts new file mode 100644 index 0000000..c32fcef --- /dev/null +++ b/web/src/lib/awam.ts @@ -0,0 +1,70 @@ +import type { ScreenRow } from "./api"; + +// Kamus istilah: kata teknis -> penjelasan santai 1 kalimat. +export const ISTILAH: Record = { + "akumulasi": "Broker-broker besar lagi borong saham ini diam-diam. Biasanya tanda mereka tahu sesuatu yang bagus.", + "distribusi": "Broker-broker besar lagi jualan saham ini pelan-pelan. Hati-hati, bisa turun.", + "foreign flow": "Uang investor luar negeri yang masuk (beli) atau keluar (jual) dari saham ini.", + "inflow": "Uang asing lagi masuk — mereka beli. Tanda kepercayaan.", + "outflow": "Uang asing lagi keluar — mereka jual. Waspada.", + "broker": "Perantara jual-beli saham (sekuritas). Gerakan mereka dibaca buat nebak arah pasar.", + "net": "Selisih total beli dikurangi total jual. Positif = lebih banyak yang beli.", + "composite": "Nilai gabungan dari semua sinyal (asing + broker + volume). Makin tinggi makin menarik.", + "screener": "Alat penyaring: cari saham yang memenuhi kriteria tertentu.", + "briefing": "Ringkasan pagi otomatis: kabar pasar hari ini dalam beberapa kalimat.", + "alert": "Notifikasi otomatis kalau saham incaranmu bergerak aneh.", + "routine": "Jadwal otomatis, misalnya terima ringkasan pasar tiap pagi jam 7.", + "portofolio": "Kumpulan semua saham yang kamu pegang.", + "diversifikasi": "Jangan taruh semua uang di satu saham — sebar biar aman.", + "rekomendasi": "Saran beli/tahan/jual dari sistem. Bukan nasihat keuangan resmi — tetap riset sendiri.", + "hold": "Tahan: jangan jual dulu, tapi belum saatnya tambah beli.", + "buy": "Beli: sinyalnya bagus, layak dipertimbangkan.", + "sell": "Jual: sinyalnya jelek, pertimbangkan keluar.", + "citations": "Sumber data: dari API mana angka ini diambil, biar bisa diverifikasi.", + "live": "Data real-time dari scheduler, bukan contoh.", + "stale": "Data sudah lama, belum diperbarui. Jangan terlalu dipercaya.", + "mover": "Saham yang harganya bergerak drastis hari ini.", + "agenda": "Jadwal penting emiten: bagi dividen, rapat pemegang saham, laporan keuangan.", + "ex-div": "Tanggal batas: kalau beli setelah tanggal ini, tidak dapat dividen periode ini.", +}; + +export type Verdict = "Dilirik" | "Dilepas" | "Netral"; + +export function verdictFor(row: ScreenRow): { verdict: Verdict; alasan: string } { + const b = (row.breakdown || {}) as Record; + const comp = Number(row.composite ?? 0); + const trend = String(b.foreign_trend ?? ""); + const broker = Number(b.broker_score ?? b.broker ?? 0); + const insider = Boolean(b.insider_buying ?? b.insider); + if (comp > 5 || (trend === "inflow" && broker > 0)) { + const bits: string[] = []; + if (trend === "inflow") bits.push("uang asing masuk"); + if (broker > 0) bits.push("broker besar borong"); + if (insider) bits.push("orang dalam ikut beli"); + return { verdict: "Dilirik", alasan: bits.length ? bits.join(", ") : "sinyal gabungan positif" }; + } + if (comp < -5 || (trend === "outflow" && broker < 0)) { + const bits: string[] = []; + if (trend === "outflow") bits.push("uang asing keluar"); + if (broker < 0) bits.push("broker besar jualan"); + return { verdict: "Dilepas", alasan: bits.length ? bits.join(", ") : "sinyal gabungan negatif" }; + } + return { verdict: "Netral", alasan: "belum ada gerakan mencolok — pantau saja" }; +} + +// Ringkas briefing mentah jadi 3 kalimat santai. +export function heroSummary(briefing: string): string[] { + const lines = briefing.split("\n").filter((l) => l.trim()); + const top = lines.filter((l) => /^\d+\./.test(l.trim())).slice(0, 3); + const foreign = lines.filter((l) => /^-/.test(l.trim()) && /Rp/i.test(l)).slice(0, 2); + const mover = lines.find((l) => /mover/i.test(l)); + const out: string[] = []; + if (top.length) { + const names = top.map((t) => t.replace(/^\d+\.\s*/, "").split(" net")[0]).join(", "); + out.push(`Pagi ini yang paling diborong: ${names}. Artinya pemain besar lagi kumpul di sana.`); + } + if (foreign.length) out.push(`Uang asing: ${foreign.map((f) => f.replace(/^-\s*/, "")).join("; ")}.`); + if (mover) out.push(`Perhatian khusus: ${mover.replace(/^.*?:\s*/, "")}.`); + if (!out.length && briefing.trim()) out.push(briefing.slice(0, 220)); + return out.slice(0, 3); +} diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index dd90f80..ed2a532 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -1,80 +1,127 @@ -import { createResource, createSignal, onCleanup, For, Show } from "solid-js"; -import { api, subscribeSSE, fmtIDR } from "../lib/api"; -import { AccumulationTable, ActivityFeed, RotationMap } from "../components/Cards"; -import { Citations } from "../components/Citations"; +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 { Term, VerdictBadge, AlasanBar, IstilahStrip } from "../components/Awam"; import { Badge } from "../components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card"; +import { Button } from "../components/ui/button"; import { Skeleton } from "../components/ui/skeleton"; import { Line } from "solid-chartjs"; import { Chart, registerables } from "chart.js"; Chart.register(...registerables); export default function Dashboard() { - const [health] = createResource(() => api.health()); - const [flow, { refetch }] = createResource(() => api.flowSummary()); - const [events, { refetch: refetchEv }] = createResource(() => api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 10))); - const [foreign] = createResource(() => api.flowForeign("BBCA").catch(() => null)); + const navigate = useNavigate(); + const [flow] = createResource(() => api.flowSummary()); + const [screen] = createResource(() => api.screen({ limit: 5 }).catch(() => null)); const [briefing] = createResource(() => api.briefing().catch(() => null)); - const [live, setLive] = createSignal([]); - const stop = subscribeSSE((ch, data) => { - setLive((p) => [`${ch}: ${data.slice(0, 120)}`, ...p].slice(0, 5)); - if (ch === "alerts" || ch === "activity") { refetch(); refetchEv(); } - }); - onCleanup(stop); - const flowChart = () => ({ + const [events] = createResource(() => api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 5)).catch(() => [])); + const [chartTiker, setChartTiker] = createSignal("BBCA"); + const [foreign] = createResource(chartTiker, (t) => api.flowForeign(t).catch(() => null)); + + const ringkasan = () => { + const p = (briefing() as { payload: string } | null)?.payload; + return p ? heroSummary(p) : []; + }; + const sorotan = () => (screen()?.rows || []).map((r) => ({ row: r, ...verdictFor(r) })); + const chartData = () => ({ labels: (foreign()?.dates || []) as string[], - datasets: [{ label: "BBCA foreign net", data: (foreign()?.nets || []) as number[] }], + datasets: [{ label: `${chartTiker()} — uang asing harian`, data: (foreign()?.nets || []) as number[] }], }); + return (
-
-

Smart Money Dashboard

-

- Credits {health()?.credits_today ?? "…"} · Scheduler {health()?.scheduler_ok ? "ok" : "down"} · Last cycle {health()?.last_cycle_at || "never"} -

-
- -

Stale: {(f) => {f}}

-
-
}> -
- - Foreign net totalAggregate across snapshots. - -

{flow() ? fmtIDR(flow()!.foreign_net_total) : "…"}

- -
-
- - - - Foreign flow (BBCA) - - No foreign-flow series yet.

}> - -

Reversal flag set.

-
-
-
- ({ ticker: String(e.ticker || ""), message: String(e.message || ""), date: String(e.date || "") }))} /> - - Live agent panel - - Waiting for SSE events…

}> -
    {(l) =>
  • {l}
  • }
-
-
-
-
- - - Morning briefing + {/* HERO: hari ini ada apa */} + + + Hari ini ada apa? 📈 + Ringkasan pasar pagi ini — tanpa perlu paham istilah ekonomi. + - No briefing yet — subscribe to the morning-briefing routine.

}> -
{(briefing() as { payload: string })?.payload?.slice(0, 800)}
+
}> + Belum ada ringkasan hari ini — coba lagi setelah scheduler jalan.

}> +
    + {(k) =>
  • • {k}
  • }
    +
+
+ +
+ + + + {/* SOROTAN SAHAM */} +
+

Saham sorotan hari ini

+
}> + Belum ada data sorotan.

}> +
+ {(s) => { + const b = (s.row.breakdown || {}) as Record; + return ( + + + {s.row.symbol} + + + +

Karena: {s.alasan}.

+ +
+ + +
+
+
+ ); + }}
+
+
+ +
+ + {/* UANG ASING PER SAHAM */} + + + Uang asing lagi ke mana? + harian — naik = asing beli, turun = asing jual. Pilih saham: + + +
+ t.ticker)}>{(t) => + + } +
+ Belum ada data grafik.

}> + +

⚠️ Arahnya baru berbalik — perhatikan beberapa hari ke depan.

+ + {/* AKTIVITAS + ANGKA MENTAH LIPAT */} +
+ + Kabar terbaru dan aktivitas sistem. + + Belum ada kabar — pasang alert biar dapat notifikasi.

}> +
    + {(e) =>
  • • {String(e.ticker || "")} — {String(e.message || "")} ({String(e.date || "")})
  • }
    +
+
+ +
+
+ + Yang paling diborongTotal beli broker hari ini: {flow() ? fmtIDR(flow()!.foreign_net_total) : "…"} + +
    + {(t) =>
  • {t.ticker}{fmtIDR(t.net_sum)} · {t.brokers} broker
  • }
    +
+
Angka detail untuk yang penasaran — keputusan ada di kartu sorotan ☝️
+
+
+
); }