# 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 */}}

Ongoing Project

{{if $proj.Label}}{{$proj.Label}}{{else}}MCU #{{$proj.McuID}}{{end}}

{{$proj.Number}} • {{$proj.CorporateName}} • {{$proj.StartDate | fmtDate}}{{$proj.EndDate | fmtDate}}

Ganti project
{{/* Section 2: Summary cards */}}

Total Patients

{{.Summary.Total}}

Peserta dalam project ini

Has PDF

{{.Summary.HasPDF}}

Laporan hasil sudah tersedia

{{/* Section 3: Filter form */}}
{{/* Section 4: Patient list */}}

Patient Result List

Data dari published_mcu_dashboard_sync

{{len .FilteredRows}} ditampilkan
{{if .FilteredRows}}
{{range .FilteredRows}}

{{.Name}}

{{.NIP}} • {{.Posisi}}

{{if .ReportDate}}

{{.ReportDate | fmtDate}}

{{end}}
{{if .FileUrl}} View PDF {{else}} No PDF {{end}}
{{end}}
{{else}}
Belum ada data untuk project ini.
{{end}}
{{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`