Initial commit
This commit is contained in:
583
docs/superpowers/plans/2026-04-30-result-menu.md
Normal file
583
docs/superpowers/plans/2026-04-30-result-menu.md
Normal file
@@ -0,0 +1,583 @@
|
||||
# Result Menu Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implementasi halaman `/result` yang menampilkan daftar peserta MCU beserta tombol View PDF, mengambil data dari `cpone_dashboard.mcu_patient` dan `cpone_dashboard.published_mcu_dashboard_sync`.
|
||||
|
||||
**Architecture:** Handler mengikuti pola `progress` — fetch semua rows, build summary, apply filter di memory, render template. PDF base URL dikonfigurasi via `.env` sebagai `PDF_BASE_URL`, disimpan di package-level var `pdfBaseURL` di package `result`.
|
||||
|
||||
**Tech Stack:** Go 1.21, Chi router, Go HTML templates (embed), Tailwind via CDN, MySQL 8 (`cpone_dashboard`).
|
||||
|
||||
**Working directory untuk semua command:** `/Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Status | Tanggung jawab |
|
||||
|------|--------|----------------|
|
||||
| `config/config.go` | **Modify** | Tambah field `PDFBaseURL string` |
|
||||
| `.env` | **Modify** | Tambah `PDF_BASE_URL=http://devcpone.aplikasi.web.id/dashboard-files/` |
|
||||
| `.env.example` | **Modify** | Tambah `PDF_BASE_URL=http://your-server/dashboard-files/` |
|
||||
| `menu/result/query.go` | **Rewrite** | Types + query + filter/summary helpers |
|
||||
| `menu/result/handler.go` | **Rewrite** | pageData, pdfBaseURL var, Index handler |
|
||||
| `main.go` | **Modify** | Wire `cfg.PDFBaseURL` ke `result.SetPDFBaseURL` |
|
||||
| `templates/result/index.html` | **Rewrite** | Full page template |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Tambah PDFBaseURL ke config
|
||||
|
||||
**Files:**
|
||||
- Modify: `config/config.go`
|
||||
- Modify: `.env`
|
||||
- Modify: `.env.example`
|
||||
|
||||
- [ ] **Step 1: Update config/config.go**
|
||||
|
||||
Tambah field `PDFBaseURL` ke struct dan `Load()`:
|
||||
|
||||
```go
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppPort string
|
||||
DBDSN string
|
||||
AuthSecret string
|
||||
PDFBaseURL string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("no .env file, reading from environment")
|
||||
}
|
||||
|
||||
return &Config{
|
||||
AppPort: getEnv("APP_PORT", "8080"),
|
||||
DBDSN: getEnv("DB_DSN", ""),
|
||||
AuthSecret: getEnv("AUTH_SECRET", "cpone-change-this-secret"),
|
||||
PDFBaseURL: getEnv("PDF_BASE_URL", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Tambah ke .env**
|
||||
|
||||
Buka `.env`, tambah baris di bawah `AUTH_SECRET`:
|
||||
|
||||
```
|
||||
PDF_BASE_URL=http://devcpone.aplikasi.web.id/dashboard-files/
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Tambah ke .env.example**
|
||||
|
||||
Buka `.env.example`, tambah baris di bawah `AUTH_SECRET`:
|
||||
|
||||
```
|
||||
PDF_BASE_URL=http://your-server/dashboard-files/
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verifikasi build**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Expected: tidak ada error output.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
git add config/config.go .env .env.example
|
||||
git commit -m "config: add PDF_BASE_URL env var"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Implementasi query.go
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `menu/result/query.go`
|
||||
|
||||
- [ ] **Step 1: Tulis query.go lengkap**
|
||||
|
||||
```go
|
||||
package result
|
||||
|
||||
import (
|
||||
"cpone-dashboard/db"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ResultRow struct {
|
||||
NIP string
|
||||
Name string
|
||||
Posisi string
|
||||
FileUrl string
|
||||
ReportDate string
|
||||
}
|
||||
|
||||
type ResultSummary struct {
|
||||
Total int
|
||||
HasPDF int
|
||||
}
|
||||
|
||||
func GetResultRows(mcuID int) ([]ResultRow, error) {
|
||||
rows, err := db.DB.Query(`
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(mp.Mcu_PatientNIP), ''), '-') AS nip,
|
||||
COALESCE(NULLIF(TRIM(mp.Mcu_PatientName), ''), '-') AS name,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(mp.Mcu_PatientDepartment), ''),
|
||||
NULLIF(TRIM(mp.Mcu_PatientDivision), ''),
|
||||
NULLIF(TRIM(mp.Mcu_PatientPosisi), ''),
|
||||
'-'
|
||||
) AS posisi,
|
||||
COALESCE(p.Published_McuDasboardFileUrl, '') AS file_url,
|
||||
CASE
|
||||
WHEN p.Published_McuDasboardFileUrl IS NOT NULL
|
||||
AND p.Published_McuDasboardFileUrl != ''
|
||||
THEN COALESCE(CAST(p.Published_McuDasboardLastUpdated AS CHAR), '')
|
||||
ELSE ''
|
||||
END AS report_date
|
||||
FROM mcu_patient mp
|
||||
LEFT JOIN published_mcu_dashboard_sync p
|
||||
ON p.Published_McuDasboardT_OrderHeaderID = mp.Mcu_PatientOrderID
|
||||
WHERE mp.Mcu_PatientMcuID = ?
|
||||
AND mp.Mcu_PatientIsActive = 'Y'
|
||||
ORDER BY
|
||||
(p.Published_McuDasboardFileUrl IS NOT NULL AND p.Published_McuDasboardFileUrl != '') DESC,
|
||||
mp.Mcu_PatientName ASC
|
||||
`, mcuID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []ResultRow
|
||||
for rows.Next() {
|
||||
var r ResultRow
|
||||
if err := rows.Scan(&r.NIP, &r.Name, &r.Posisi, &r.FileUrl, &r.ReportDate); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func BuildResultSummary(rows []ResultRow) ResultSummary {
|
||||
s := ResultSummary{Total: len(rows)}
|
||||
for _, r := range rows {
|
||||
if r.FileUrl != "" {
|
||||
s.HasPDF++
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow {
|
||||
search = strings.ToLower(strings.TrimSpace(search))
|
||||
filter = strings.TrimSpace(filter)
|
||||
if search == "" && filter == "" {
|
||||
return rows
|
||||
}
|
||||
out := make([]ResultRow, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
switch filter {
|
||||
case "has_pdf":
|
||||
if r.FileUrl == "" {
|
||||
continue
|
||||
}
|
||||
case "no_pdf":
|
||||
if r.FileUrl != "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if search != "" {
|
||||
hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi)
|
||||
if !strings.Contains(hay, search) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verifikasi build**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Expected: tidak ada error.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
git add menu/result/query.go
|
||||
git commit -m "result: implement query, summary, and filter helpers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Implementasi handler.go
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `menu/result/handler.go`
|
||||
|
||||
- [ ] **Step 1: Tulis handler.go lengkap**
|
||||
|
||||
```go
|
||||
package result
|
||||
|
||||
import (
|
||||
"cpone-dashboard/menu/auth"
|
||||
"cpone-dashboard/menu/projects"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var tmpl *template.Template
|
||||
var pdfBaseURL string
|
||||
|
||||
func SetTemplates(t *template.Template) { tmpl = t }
|
||||
func SetPDFBaseURL(u string) { pdfBaseURL = u }
|
||||
|
||||
type pageData struct {
|
||||
Username string
|
||||
CurrentProject projects.ProjectItem
|
||||
Search string
|
||||
Filter string
|
||||
Rows []ResultRow
|
||||
FilteredRows []ResultRow
|
||||
Summary ResultSummary
|
||||
PDFBaseURL string
|
||||
}
|
||||
|
||||
func Index(w http.ResponseWriter, r *http.Request) {
|
||||
username := auth.Username(r)
|
||||
mcuID := auth.SelectedProjectID(r)
|
||||
if mcuID == 0 {
|
||||
http.Redirect(w, r, "/projects", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
project, ok, err := projects.GetUserProject(username, mcuID)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/projects", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := GetResultRows(mcuID)
|
||||
if err != nil {
|
||||
http.Error(w, "query error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
summary := BuildResultSummary(rows)
|
||||
search := r.URL.Query().Get("search")
|
||||
filter := r.URL.Query().Get("filter")
|
||||
filteredRows := FilterResultRows(rows, search, filter)
|
||||
|
||||
t := tmpl
|
||||
if t == nil {
|
||||
http.Error(w, "template not ready", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "base", pageData{
|
||||
Username: username,
|
||||
CurrentProject: project,
|
||||
Search: search,
|
||||
Filter: filter,
|
||||
Rows: rows,
|
||||
FilteredRows: filteredRows,
|
||||
Summary: summary,
|
||||
PDFBaseURL: pdfBaseURL,
|
||||
}); err != nil {
|
||||
http.Error(w, "template error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verifikasi build**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Expected: tidak ada error.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
git add menu/result/handler.go
|
||||
git commit -m "result: implement Index handler with filter and summary"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Wire PDFBaseURL di main.go
|
||||
|
||||
**Files:**
|
||||
- Modify: `main.go` — tambah `result.SetPDFBaseURL(cfg.PDFBaseURL)` setelah `result.SetTemplates(...)`
|
||||
|
||||
- [ ] **Step 1: Temukan baris result.SetTemplates di main.go**
|
||||
|
||||
Cari baris (sekitar line 254):
|
||||
```go
|
||||
result.SetTemplates(newPageTmpl("templates/result/index.html"))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Tambah SetPDFBaseURL tepat setelahnya**
|
||||
|
||||
```go
|
||||
result.SetTemplates(newPageTmpl("templates/result/index.html"))
|
||||
result.SetPDFBaseURL(cfg.PDFBaseURL)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verifikasi build**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Expected: tidak ada error.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
git add main.go
|
||||
git commit -m "main: wire PDF_BASE_URL into result handler"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Implementasi template
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `templates/result/index.html`
|
||||
|
||||
- [ ] **Step 1: Tulis template lengkap**
|
||||
|
||||
```html
|
||||
{{define "title"}}Result Reports — CpOne{{end}}
|
||||
{{define "header-title"}}Consolidated Result Reports{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
{{$proj := .CurrentProject}}
|
||||
|
||||
{{/* Section 1: Current project */}}
|
||||
<section class="card p-5">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-brand-500">Ongoing Project</p>
|
||||
<h2 class="mt-1 text-lg font-semibold text-slate-900">
|
||||
{{if $proj.Label}}{{$proj.Label}}{{else}}MCU #{{$proj.McuID}}{{end}}
|
||||
</h2>
|
||||
<p class="mt-0.5 text-sm text-slate-500">
|
||||
{{$proj.Number}} • {{$proj.CorporateName}} •
|
||||
<span class="num">{{$proj.StartDate | fmtDate}}</span> – <span class="num">{{$proj.EndDate | fmtDate}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="/projects" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 transition hover:border-brand-400 hover:text-brand-600">
|
||||
Ganti project
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* Section 2: Summary cards */}}
|
||||
<section class="grid gap-4 sm:grid-cols-2">
|
||||
<article class="card p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Total Patients</p>
|
||||
<p class="num mt-2 text-3xl font-semibold text-slate-900">{{.Summary.Total}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Peserta dalam project ini</p>
|
||||
</article>
|
||||
<article class="card p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Has PDF</p>
|
||||
<p class="num mt-2 text-3xl font-semibold text-brand-500">{{.Summary.HasPDF}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Laporan hasil sudah tersedia</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{/* Section 3: Filter form */}}
|
||||
<section class="card p-4">
|
||||
<form method="get" action="/result" class="grid gap-3 md:grid-cols-3">
|
||||
<div class="md:col-span-2">
|
||||
<label for="search" class="mb-2 block text-sm font-medium text-slate-600">Search Patient</label>
|
||||
<input id="search" name="search" value="{{.Search}}" type="text" placeholder="Nama atau Employee ID"
|
||||
class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm outline-none transition focus:border-brand-400 focus:ring-2 focus:ring-brand-200"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter" class="mb-2 block text-sm font-medium text-slate-600">Status PDF</label>
|
||||
<select id="filter" name="filter"
|
||||
class="w-full rounded-xl border border-slate-200 px-4 py-3 text-sm outline-none transition focus:border-brand-400 focus:ring-2 focus:ring-brand-200">
|
||||
<option value="" {{if eq .Filter ""}}selected{{end}}>All</option>
|
||||
<option value="has_pdf" {{if eq .Filter "has_pdf"}}selected{{end}}>Has PDF</option>
|
||||
<option value="no_pdf" {{if eq .Filter "no_pdf"}}selected{{end}}>No PDF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex justify-end">
|
||||
<button type="submit" class="rounded-xl bg-brand-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-600">
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{/* Section 4: Patient list */}}
|
||||
<section class="card overflow-hidden">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-slate-700">Patient Result List</h2>
|
||||
<p class="text-xs text-slate-400">Data dari published_mcu_dashboard_sync</p>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-slate-400">{{len .FilteredRows}} ditampilkan</span>
|
||||
</div>
|
||||
|
||||
{{if .FilteredRows}}
|
||||
<div class="hidden overflow-x-auto md:block">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-medium">Employee ID</th>
|
||||
<th class="px-4 py-3 font-medium">Patient</th>
|
||||
<th class="px-4 py-3 font-medium">Department</th>
|
||||
<th class="px-4 py-3 font-medium">Report Date</th>
|
||||
<th class="px-4 py-3 font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{{range .FilteredRows}}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="num px-4 py-3 text-slate-500">{{.NIP}}</td>
|
||||
<td class="px-4 py-3 font-medium text-slate-700">{{.Name}}</td>
|
||||
<td class="px-4 py-3 text-slate-500">{{.Posisi}}</td>
|
||||
<td class="num px-4 py-3 text-slate-500">
|
||||
{{if .ReportDate}}{{.ReportDate | fmtDate}}{{else}}<span class="text-slate-300">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{if .FileUrl}}
|
||||
<a href="{{$.PDFBaseURL}}{{.FileUrl}}" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-brand-500 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-brand-600">
|
||||
View PDF
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="text-xs text-slate-300">—</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 p-4 md:hidden">
|
||||
{{range .FilteredRows}}
|
||||
<article class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-700">{{.Name}}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">{{.NIP}} • {{.Posisi}}</p>
|
||||
{{if .ReportDate}}<p class="mt-0.5 num text-xs text-slate-400">{{.ReportDate | fmtDate}}</p>{{end}}
|
||||
</div>
|
||||
{{if .FileUrl}}
|
||||
<a href="{{$.PDFBaseURL}}{{.FileUrl}}" target="_blank"
|
||||
class="shrink-0 rounded-lg bg-brand-500 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-brand-600">
|
||||
View PDF
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="text-xs text-slate-300">No PDF</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="px-5 py-10 text-center text-sm text-slate-400">
|
||||
Belum ada data untuk project ini.
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build dan cek tidak ada syntax error template**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Expected: tidak ada error.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
git add templates/result/index.html
|
||||
git commit -m "result: implement full page template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Manual verification
|
||||
|
||||
- [ ] **Step 1: Pastikan SSH tunnel aktif, lalu jalankan app**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
make start
|
||||
```
|
||||
|
||||
Expected: `server running on :8080`
|
||||
|
||||
- [ ] **Step 2: Buka browser, login, pilih project**
|
||||
|
||||
Navigasi ke `http://localhost:8080/result`. Harus tampil:
|
||||
- Header "Consolidated Result Reports"
|
||||
- Summary cards: Total Patients dan Has PDF (sesuai data di DB)
|
||||
- Table daftar pasien
|
||||
|
||||
- [ ] **Step 3: Verifikasi tombol View PDF**
|
||||
|
||||
Row pertama (NIP sesuai data) harus ada tombol "View PDF" berwarna brand-500. Klik — harus buka PDF di tab baru dengan URL `http://devcpone.aplikasi.web.id/dashboard-files/2024/09/R2409170003_resume_individu.pdf`.
|
||||
|
||||
- [ ] **Step 4: Verifikasi filter**
|
||||
|
||||
Pilih filter "Has PDF" → hanya 1 row tampil. Pilih "No PDF" → semua row selain 1 tampil. Ketik nama di search → filter berjalan.
|
||||
|
||||
- [ ] **Step 5: Deploy ke server**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_CPONE_DASHBOARD/cpone-dashboard
|
||||
make deploy
|
||||
```
|
||||
|
||||
Expected: `deployed to one@devcpone.aplikasi.web.id:/home/one/project/cpone-dashboard`
|
||||
107
docs/superpowers/specs/2026-04-27-mcu-dashboard-design.md
Normal file
107
docs/superpowers/specs/2026-04-27-mcu-dashboard-design.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# MCU Dashboard (cpone-dashboard) — Design Spec
|
||||
Date: 2026-04-27
|
||||
|
||||
## Overview
|
||||
Dashboard live monitoring MCU (Medical Check-Up) untuk laboratorium klinik CpOne. Menampilkan data real-time dari kegiatan MCU korporat: KPI harian, TAT, status station, arrival tracking, progress pemeriksaan, abnormal monitoring, dan laporan hasil.
|
||||
|
||||
## Data Architecture
|
||||
```
|
||||
cpone (main DB, Server A)
|
||||
↓ inject/ETL (proyek terpisah)
|
||||
cpone_dashboard (Server A)
|
||||
↓ MySQL replication (otomatis)
|
||||
cpone_dashboard (Server B — production)
|
||||
↓ dibaca oleh
|
||||
Go Dashboard App (Server B)
|
||||
```
|
||||
|
||||
Dashboard app **hanya** konek ke `cpone_dashboard` lokal. Zero dependency ke `cpone`.
|
||||
|
||||
## Tech Stack
|
||||
- **Backend**: Go 1.21, framework Chi (router lightweight)
|
||||
- **Frontend**: Go HTML templates (embed ke binary), HTMX via CDN, ECharts via CDN, Tailwind via CDN
|
||||
- **Database**: MySQL 8.0, single connection ke `cpone_dashboard`
|
||||
- **Build**: Cross-compile di Mac (`GOOS=linux GOARCH=amd64`), deploy binary ke server
|
||||
- **Primary color**: `#3b50a0`
|
||||
|
||||
## Pages
|
||||
1. **Login** — autentikasi user
|
||||
2. **Dashboard** — KPI cards, TAT harian, station status table, arrival list, trend chart (HTMX polling tiap 10s)
|
||||
3. **Arrival Tracking** — daftar peserta check-in
|
||||
4. **Observation Progress** — progress per station pemeriksaan
|
||||
5. **Abnormal Monitoring** — hasil pemeriksaan dengan flag abnormal
|
||||
6. **Result Reports** — laporan hasil konsolidasi per peserta
|
||||
|
||||
## Folder Structure
|
||||
```
|
||||
cpone-dashboard/
|
||||
├── main.go
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── .env ← DB DSN, port, dll
|
||||
├── .env.example
|
||||
├── Makefile ← make build, make deploy
|
||||
│
|
||||
├── config/
|
||||
│ └── config.go
|
||||
│
|
||||
├── db/
|
||||
│ └── db.go ← single connection ke cpone_dashboard
|
||||
│
|
||||
├── menu/
|
||||
│ ├── dashboard/
|
||||
│ │ ├── handler.go
|
||||
│ │ ├── query.go
|
||||
│ │ └── route.go
|
||||
│ ├── arrival/
|
||||
│ │ ├── handler.go
|
||||
│ │ ├── query.go
|
||||
│ │ └── route.go
|
||||
│ ├── progress/
|
||||
│ │ ├── handler.go
|
||||
│ │ ├── query.go
|
||||
│ │ └── route.go
|
||||
│ ├── abnormal/
|
||||
│ │ ├── handler.go
|
||||
│ │ ├── query.go
|
||||
│ │ └── route.go
|
||||
│ └── result/
|
||||
│ ├── handler.go
|
||||
│ ├── query.go
|
||||
│ └── route.go
|
||||
│
|
||||
├── templates/
|
||||
│ ├── layout/
|
||||
│ │ └── base.html
|
||||
│ ├── dashboard/
|
||||
│ │ ├── index.html
|
||||
│ │ └── partials/
|
||||
│ │ ├── kpi.html
|
||||
│ │ ├── stations.html
|
||||
│ │ └── arrivals.html
|
||||
│ ├── arrival/
|
||||
│ │ └── index.html
|
||||
│ ├── progress/
|
||||
│ │ └── index.html
|
||||
│ ├── abnormal/
|
||||
│ │ └── index.html
|
||||
│ └── result/
|
||||
│ └── index.html
|
||||
│
|
||||
└── static/
|
||||
└── css/
|
||||
└── custom.css
|
||||
```
|
||||
|
||||
## Deploy Flow
|
||||
```bash
|
||||
make deploy
|
||||
# = GOOS=linux GOARCH=amd64 go build -o cpone-dashboard .
|
||||
# + scp cpone-dashboard one@devcpone.aplikasi.web.id:/home/one/project/cpone-dashboard/
|
||||
# + ssh ... restart process
|
||||
```
|
||||
|
||||
## Out of Scope
|
||||
- Inject/ETL dari `cpone` ke `cpone_dashboard` (proyek terpisah)
|
||||
- MySQL replication setup
|
||||
- Multi-tenancy / multi-server config
|
||||
114
docs/superpowers/specs/2026-04-30-result-menu-design.md
Normal file
114
docs/superpowers/specs/2026-04-30-result-menu-design.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Result Menu — Design Spec
|
||||
Date: 2026-04-30
|
||||
|
||||
## Overview
|
||||
Halaman `/result` menampilkan daftar peserta MCU beserta tombol "View PDF" untuk membuka laporan hasil konsolidasi. Data diambil sepenuhnya dari `cpone_dashboard` (zero dependency ke `cpone`).
|
||||
|
||||
## Data Sources
|
||||
Semua tabel ada di `cpone_dashboard`:
|
||||
- `mcu_patient` — data peserta (NIP, nama, posisi/dept, order ID)
|
||||
- `published_mcu_dashboard_sync` — file URL PDF per peserta
|
||||
|
||||
Join key: `mcu_patient.Mcu_PatientOrderID = published_mcu_dashboard_sync.Published_McuDasboardT_OrderHeaderID`
|
||||
|
||||
## Config / Env
|
||||
Tambah key baru ke `.env`, `.env.example`, dan `config/config.go`:
|
||||
```
|
||||
PDF_BASE_URL=http://devcpone.aplikasi.web.id/dashboard-files/
|
||||
```
|
||||
Field `PDFBaseURL string` ditambah ke struct `Config`. Nilai ini di-passing ke `result` handler saat setup di `main.go`.
|
||||
|
||||
## Backend — `menu/result/`
|
||||
|
||||
### query.go
|
||||
```go
|
||||
type ResultRow struct {
|
||||
NIP string
|
||||
Name string
|
||||
Posisi string
|
||||
FileUrl string // kosong jika belum ada PDF
|
||||
ReportDate string // Published_McuDasboardLastUpdated
|
||||
}
|
||||
|
||||
type ResultSummary struct {
|
||||
Total int
|
||||
HasPDF int
|
||||
}
|
||||
```
|
||||
|
||||
Query:
|
||||
```sql
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(mp.Mcu_PatientNIP), ''), '-') AS nip,
|
||||
COALESCE(NULLIF(TRIM(mp.Mcu_PatientName), ''), '-') AS name,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(mp.Mcu_PatientDepartment), ''),
|
||||
NULLIF(TRIM(mp.Mcu_PatientDivision), ''),
|
||||
NULLIF(TRIM(mp.Mcu_PatientPosisi), ''),
|
||||
'-'
|
||||
) AS posisi,
|
||||
COALESCE(p.Published_McuDasboardFileUrl, '') AS file_url,
|
||||
CASE
|
||||
WHEN p.Published_McuDasboardFileUrl IS NOT NULL AND p.Published_McuDasboardFileUrl != ''
|
||||
THEN COALESCE(p.Published_McuDasboardLastUpdated, '')
|
||||
ELSE ''
|
||||
END AS report_date
|
||||
FROM mcu_patient mp
|
||||
LEFT JOIN published_mcu_dashboard_sync p
|
||||
ON p.Published_McuDasboardT_OrderHeaderID = mp.Mcu_PatientOrderID
|
||||
WHERE mp.Mcu_PatientMcuID = ?
|
||||
AND mp.Mcu_PatientIsActive = 'Y'
|
||||
ORDER BY
|
||||
(p.Published_McuDasboardFileUrl IS NOT NULL AND p.Published_McuDasboardFileUrl != '') DESC,
|
||||
mp.Mcu_PatientName ASC
|
||||
```
|
||||
|
||||
Helper functions:
|
||||
- `BuildResultSummary(rows []ResultRow) ResultSummary`
|
||||
- `FilterResultRows(rows []ResultRow, search, filter string) []ResultRow`
|
||||
- filter values: `""` (all), `"has_pdf"`, `"no_pdf"`
|
||||
|
||||
### handler.go
|
||||
`pageData` struct:
|
||||
```go
|
||||
type pageData struct {
|
||||
Username string
|
||||
CurrentProject projects.ProjectItem
|
||||
Search string
|
||||
Filter string
|
||||
Rows []ResultRow
|
||||
FilteredRows []ResultRow
|
||||
Summary ResultSummary
|
||||
PDFBaseURL string
|
||||
}
|
||||
```
|
||||
|
||||
Handler `Index` mengikuti pola progress: redirect ke `/projects` jika belum pilih project, fetch rows, build summary, apply filter, render template.
|
||||
|
||||
`PDFBaseURL` di-inject saat `SetTemplates` — tambah fungsi `SetPDFBaseURL(url string)` di package result.
|
||||
|
||||
### route.go
|
||||
Tidak berubah — sudah ada `r.Get("/", Index)`.
|
||||
|
||||
## Template — `templates/result/index.html`
|
||||
|
||||
**Section 1 — Current project card**
|
||||
Sama persis dengan progress/arrival: nama project, nomor, tombol "Ganti project".
|
||||
|
||||
**Section 2 — Summary cards (2 cards)**
|
||||
- Total Patients
|
||||
- Has PDF (count `FileUrl != ""`)
|
||||
|
||||
**Section 3 — Filter form**
|
||||
- Search input (nama atau NIP)
|
||||
- Dropdown: All / Has PDF / No PDF
|
||||
- Tombol Filter
|
||||
|
||||
**Section 4 — Patient list**
|
||||
- Desktop: table dengan kolom NIP, Nama, Posisi/Dept, Report Date, Action
|
||||
- Mobile: card stack
|
||||
- Action: tombol `View PDF` (buka tab baru) jika `FileUrl != ""`, teks `—` jika kosong
|
||||
- PDF full URL: `PDFBaseURL + FileUrl`
|
||||
|
||||
## Referensi Visual
|
||||
`/PLAN/draft-cpone/06-result.html` — warna dan layout mengikuti color scheme brand yang ada (`brand-500`, `slate-*`), bukan warna `diagnos-*` dari draft.
|
||||
Reference in New Issue
Block a user