Improve mobile UI and add infinite scroll

This commit is contained in:
sas.fajri
2026-05-04 10:44:38 +07:00
parent b07f04217e
commit 7c378f78bf
17 changed files with 729 additions and 115 deletions

View File

@@ -6,11 +6,14 @@ import (
"encoding/json"
"html/template"
"net/http"
"strconv"
)
var tmpl *template.Template
var basePath string
const defaultPageSize = 30
func SetTemplates(t *template.Template) { tmpl = t }
func SetBasePath(p string) { basePath = p }
@@ -28,6 +31,12 @@ type pageData struct {
DepartmentOptions []string
OverviewJSON template.JS
DepartmentJSON template.JS
Page int
PageSize int
HasMore bool
VisibleCount int
TotalFiltered int
NextPage int
}
func toJS(v interface{}) template.JS {
@@ -35,33 +44,36 @@ func toJS(v interface{}) template.JS {
return template.JS(b)
}
func Index(w http.ResponseWriter, r *http.Request) {
func parsePage(q string) int {
p, err := strconv.Atoi(q)
if err != nil || p < 1 {
return 1
}
return p
}
func buildArrivalData(r *http.Request) (pageData, int, error) {
username := auth.Username(r)
mcuID := auth.SelectedProjectID(r)
if mcuID == 0 {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
return pageData{}, http.StatusSeeOther, nil
}
project, ok, err := projects.GetUserProject(username, mcuID)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
if !ok {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
return pageData{}, http.StatusSeeOther, nil
}
dates, err := GetArrivalDates(mcuID)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
selectedDate := activeDateOrLatest(dates, r.URL.Query().Get("date"), project.StartDate)
rows, err := GetArrivalRows(mcuID, selectedDate)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
summary, deptStats := BuildArrivalStats(rows)
filteredRows := FilterArrivalRows(rows, r.URL.Query().Get("search"), r.URL.Query().Get("dept"))
@@ -69,14 +81,12 @@ func Index(w http.ResponseWriter, r *http.Request) {
stationMap, err := GetStationProgress(mcuID, selectedDate)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
for i := range filteredRows {
filteredRows[i].Stations = stationMap[filteredRows[i].PreregisterID]
}
// Chart 1: double donut — inner: checked-in vs pending, outer: total per posisi/dept
outerDepts := []map[string]any{}
for _, d := range deptStats {
if d.Total > 0 {
@@ -89,7 +99,6 @@ func Index(w http.ResponseWriter, r *http.Request) {
"depts": outerDepts,
}
// Chart 2: per-station patient count bar chart
stationStats := BuildStationChart(stationMap)
stationLabels := make([]string, len(stationStats))
stationCounts := make([]int, len(stationStats))
@@ -102,12 +111,14 @@ func Index(w http.ResponseWriter, r *http.Request) {
"counts": stationCounts,
}
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
page := parsePage(r.URL.Query().Get("page"))
pagedRows, hasMore := PaginateArrivalRows(filteredRows, page, defaultPageSize)
visibleCount := page * defaultPageSize
if visibleCount > len(filteredRows) {
visibleCount = len(filteredRows)
}
data := pageData{
return pageData{
Username: username,
CurrentProject: project,
Date: selectedDate,
@@ -115,14 +126,61 @@ func Index(w http.ResponseWriter, r *http.Request) {
Search: r.URL.Query().Get("search"),
Department: r.URL.Query().Get("dept"),
Rows: rows,
FilteredRows: filteredRows,
FilteredRows: pagedRows,
Summary: summary,
Departments: deptStats,
DepartmentOptions: deptOptions,
OverviewJSON: toJS(overview),
DepartmentJSON: toJS(stationChart),
Page: page,
PageSize: defaultPageSize,
HasMore: hasMore,
VisibleCount: visibleCount,
TotalFiltered: len(filteredRows),
NextPage: page + 1,
}, http.StatusOK, nil
}
func Index(w http.ResponseWriter, r *http.Request) {
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
}
data, status, err := buildArrivalData(r)
if err != nil {
http.Error(w, "query error", status)
return
}
if status == http.StatusSeeOther {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
}
if err := t.ExecuteTemplate(w, "base", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}
func List(w http.ResponseWriter, r *http.Request) {
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
}
data, status, err := buildArrivalData(r)
if err != nil {
http.Error(w, "query error", status)
return
}
if status == http.StatusSeeOther {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if err := t.ExecuteTemplate(w, "arrival-list-chunk", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}

View File

@@ -270,6 +270,24 @@ func FilterArrivalRows(rows []ArrivalRow, search, dept string) []ArrivalRow {
return out
}
func PaginateArrivalRows(rows []ArrivalRow, page, pageSize int) ([]ArrivalRow, bool) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 30
}
start := (page - 1) * pageSize
if start >= len(rows) {
return []ArrivalRow{}, false
}
end := start + pageSize
if end > len(rows) {
end = len(rows)
}
return rows[start:end], end < len(rows)
}
func UniqueDepartments(rows []ArrivalRow) []string {
seen := map[string]struct{}{}
var out []string

View File

@@ -4,4 +4,5 @@ import "github.com/go-chi/chi/v5"
func Routes(r chi.Router) {
r.Get("/", Index)
r.Get("/list", List)
}

View File

@@ -5,11 +5,14 @@ import (
"cpone-dashboard/menu/projects"
"html/template"
"net/http"
"strconv"
)
var tmpl *template.Template
var basePath string
const defaultPageSize = 30
func SetTemplates(t *template.Template) { tmpl = t }
func SetBasePath(p string) { basePath = p }
@@ -23,52 +26,108 @@ type pageData struct {
Summary ProgressSummary
ValidatedPct int
PublishedPct int
Page int
PageSize int
HasMore bool
VisibleCount int
TotalFiltered int
NextPage int
}
func Index(w http.ResponseWriter, r *http.Request) {
func parsePage(q string) int {
p, err := strconv.Atoi(q)
if err != nil || p < 1 {
return 1
}
return p
}
func buildProgressData(r *http.Request) (pageData, int, error) {
username := auth.Username(r)
mcuID := auth.SelectedProjectID(r)
if mcuID == 0 {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
return pageData{}, http.StatusSeeOther, nil
}
project, ok, err := projects.GetUserProject(username, mcuID)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
if !ok {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
return pageData{}, http.StatusSeeOther, nil
}
rows, err := GetProgressRows(mcuID)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
summary := BuildProgressSummary(rows)
search := r.URL.Query().Get("search")
status := r.URL.Query().Get("status")
filteredRows := FilterProgressRows(rows, search, status)
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
page := parsePage(r.URL.Query().Get("page"))
pagedRows, hasMore := PaginateProgressRows(filteredRows, page, defaultPageSize)
visibleCount := page * defaultPageSize
if visibleCount > len(filteredRows) {
visibleCount = len(filteredRows)
}
if err := t.ExecuteTemplate(w, "base", pageData{
return pageData{
Username: username,
CurrentProject: project,
Search: search,
Status: status,
Rows: rows,
FilteredRows: filteredRows,
FilteredRows: pagedRows,
Summary: summary,
ValidatedPct: Pct(summary.Validated, summary.Total),
PublishedPct: Pct(summary.Published, summary.Total),
}); err != nil {
Page: page,
PageSize: defaultPageSize,
HasMore: hasMore,
VisibleCount: visibleCount,
TotalFiltered: len(filteredRows),
NextPage: page + 1,
}, http.StatusOK, nil
}
func Index(w http.ResponseWriter, r *http.Request) {
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
}
data, status, err := buildProgressData(r)
if err != nil {
http.Error(w, "query error", status)
return
}
if status == http.StatusSeeOther {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
}
if err := t.ExecuteTemplate(w, "base", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}
func List(w http.ResponseWriter, r *http.Request) {
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
}
data, status, err := buildProgressData(r)
if err != nil {
http.Error(w, "query error", status)
return
}
if status == http.StatusSeeOther {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if err := t.ExecuteTemplate(w, "progress-list-chunk", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}

View File

@@ -118,3 +118,21 @@ func Pct(num, total int) int {
}
return num * 100 / total
}
func PaginateProgressRows(rows []ProgressRow, page, pageSize int) ([]ProgressRow, bool) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 30
}
start := (page - 1) * pageSize
if start >= len(rows) {
return []ProgressRow{}, false
}
end := start + pageSize
if end > len(rows) {
end = len(rows)
}
return rows[start:end], end < len(rows)
}

View File

@@ -4,4 +4,5 @@ import "github.com/go-chi/chi/v5"
func Routes(r chi.Router) {
r.Get("/", Index)
r.Get("/list", List)
}

View File

@@ -5,12 +5,15 @@ import (
"cpone-dashboard/menu/projects"
"html/template"
"net/http"
"strconv"
)
var tmpl *template.Template
var pdfBaseURL string
var basePath string
const defaultPageSize = 30
func SetTemplates(t *template.Template) { tmpl = t }
func SetPDFBaseURL(u string) { pdfBaseURL = u }
func SetBasePath(p string) { basePath = p }
@@ -24,51 +27,108 @@ type pageData struct {
FilteredRows []ResultRow
Summary ResultSummary
PDFBaseURL string
Page int
PageSize int
HasMore bool
VisibleCount int
TotalFiltered int
NextPage int
}
func Index(w http.ResponseWriter, r *http.Request) {
func parsePage(q string) int {
p, err := strconv.Atoi(q)
if err != nil || p < 1 {
return 1
}
return p
}
func buildResultData(r *http.Request) (pageData, int, error) {
username := auth.Username(r)
mcuID := auth.SelectedProjectID(r)
if mcuID == 0 {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
return pageData{}, http.StatusSeeOther, nil
}
project, ok, err := projects.GetUserProject(username, mcuID)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
if !ok {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
return pageData{}, http.StatusSeeOther, nil
}
rows, err := GetResultRows(mcuID)
if err != nil {
http.Error(w, "query error", http.StatusInternalServerError)
return
return pageData{}, http.StatusInternalServerError, err
}
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
page := parsePage(r.URL.Query().Get("page"))
pagedRows, hasMore := PaginateResultRows(filteredRows, page, defaultPageSize)
visibleCount := page * defaultPageSize
if visibleCount > len(filteredRows) {
visibleCount = len(filteredRows)
}
if err := t.ExecuteTemplate(w, "base", pageData{
return pageData{
Username: username,
CurrentProject: project,
Search: search,
Filter: filter,
Rows: rows,
FilteredRows: filteredRows,
FilteredRows: pagedRows,
Summary: summary,
PDFBaseURL: pdfBaseURL,
}); err != nil {
Page: page,
PageSize: defaultPageSize,
HasMore: hasMore,
VisibleCount: visibleCount,
TotalFiltered: len(filteredRows),
NextPage: page + 1,
}, http.StatusOK, nil
}
func Index(w http.ResponseWriter, r *http.Request) {
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
}
data, status, err := buildResultData(r)
if err != nil {
http.Error(w, "query error", status)
return
}
if status == http.StatusSeeOther {
http.Redirect(w, r, basePath+"/projects", http.StatusSeeOther)
return
}
if err := t.ExecuteTemplate(w, "base", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}
func List(w http.ResponseWriter, r *http.Request) {
t := tmpl
if t == nil {
http.Error(w, "template not ready", http.StatusInternalServerError)
return
}
data, status, err := buildResultData(r)
if err != nil {
http.Error(w, "query error", status)
return
}
if status == http.StatusSeeOther {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if err := t.ExecuteTemplate(w, "result-list-chunk", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}

View File

@@ -99,3 +99,21 @@ func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow {
}
return out
}
func PaginateResultRows(rows []ResultRow, page, pageSize int) ([]ResultRow, bool) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 30
}
start := (page - 1) * pageSize
if start >= len(rows) {
return []ResultRow{}, false
}
end := start + pageSize
if end > len(rows) {
end = len(rows)
}
return rows[start:end], end < len(rows)
}

View File

@@ -4,4 +4,5 @@ import "github.com/go-chi/chi/v5"
func Routes(r chi.Router) {
r.Get("/", Index)
r.Get("/list", List)
}