Initial commit

This commit is contained in:
sas.fajri
2026-04-30 14:27:01 +07:00
commit e29e943c27
70 changed files with 8909 additions and 0 deletions

View 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}} &bull; {{$proj.CorporateName}} &bull;
<span class="num">{{$proj.StartDate | fmtDate}}</span> &ndash; <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}} &bull; {{.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`

View 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

View 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.