Files
cpone_dashboard/cpone-dashboard/main.go
2026-04-30 14:27:01 +07:00

311 lines
7.7 KiB
Go

package main
import (
"cpone-dashboard/config"
"cpone-dashboard/db"
"cpone-dashboard/menu/abnormal"
"cpone-dashboard/menu/arrival"
"cpone-dashboard/menu/auth"
"cpone-dashboard/menu/dashboard"
"cpone-dashboard/menu/progress"
"cpone-dashboard/menu/projects"
"cpone-dashboard/menu/result"
"embed"
"html/template"
"io/fs"
"log"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
//go:embed templates
var templateFS embed.FS
//go:embed static
var staticFS embed.FS
func main() {
cfg := config.Load()
if err := db.Connect(cfg.DBDSN); err != nil {
log.Fatalf("db connect: %v", err)
}
defer db.DB.Close()
dashboard.SetTemplateFuncs(template.FuncMap{
"div": func(a, b int) int {
if b == 0 {
return 0
}
return a / b
},
"mod": func(a, b int) int { return a % b },
"pct": func(a, b int) float64 {
if b == 0 {
return 0
}
return float64(a) / float64(b) * 100
},
"stationShort": func(s string) string {
s = strings.TrimPrefix(s, "Sample Station ")
s = strings.TrimPrefix(s, "sample station ")
return s
},
"fmtDate": func(s string) string {
if s == "" {
return ""
}
layouts := []string{
"2006-01-02",
"2006-01-02 15:04:05",
time.RFC3339,
"02/01/2006",
"02/01/2006 15:04:05",
}
for _, layout := range layouts {
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
return t.Format("02/01/2006")
}
}
if len(s) >= 10 {
return s[8:10] + "/" + s[5:7] + "/" + s[0:4]
}
return s
},
"fmtDateTime": func(dateStr, timeStr string) string {
if dateStr == "" {
return ""
}
dateLayouts := []string{
"2006-01-02",
"2006-01-02 15:04:05",
"02/01/2006",
"02/01/2006 15:04:05",
time.RFC3339,
}
var datePart time.Time
for _, layout := range dateLayouts {
if t, err := time.ParseInLocation(layout, dateStr, time.Local); err == nil {
datePart = t
break
}
}
if datePart.IsZero() {
if len(dateStr) >= 10 {
dateStr = dateStr[8:10] + "/" + dateStr[5:7] + "/" + dateStr[0:4]
}
} else {
dateStr = datePart.Format("02/01/2006")
}
if timeStr == "" {
return dateStr
}
timeLayouts := []string{"15:04:05", "15:04"}
var timePart string
for _, layout := range timeLayouts {
if t, err := time.Parse(layout, timeStr); err == nil {
timePart = t.Format("15:04:05")
break
}
}
if timePart == "" {
timePart = timeStr
}
return dateStr + " " + timePart
},
"initials": func(name string) string {
parts := strings.Fields(name)
if len(parts) == 0 {
return "?"
}
if len(parts) == 1 {
return strings.ToUpper(string([]rune(parts[0])[:1]))
}
return strings.ToUpper(string([]rune(parts[0])[:1]) + string([]rune(parts[len(parts)-1])[:1]))
},
"slice": func(args ...int) []int { return args },
"seq": func(n int) []int {
s := make([]int, n)
for i := range s {
s[i] = i
}
return s
},
})
auth.Init(&templateFS, cfg.AuthSecret)
projects.SetTemplateFS(&templateFS)
pageFuncs := template.FuncMap{
"div": func(a, b int) int {
if b == 0 {
return 0
}
return a / b
},
"mod": func(a, b int) int { return a % b },
"pct": func(a, b int) float64 {
if b == 0 {
return 0
}
return float64(a) / float64(b) * 100
},
"stationShort": func(s string) string {
s = strings.TrimPrefix(s, "Sample Station ")
s = strings.TrimPrefix(s, "sample station ")
return s
},
"fmtDate": func(s string) string {
if s == "" {
return ""
}
layouts := []string{
"2006-01-02",
"2006-01-02 15:04:05",
time.RFC3339,
"02/01/2006",
"02/01/2006 15:04:05",
}
for _, layout := range layouts {
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
return t.Format("02/01/2006")
}
}
if len(s) >= 10 {
return s[8:10] + "/" + s[5:7] + "/" + s[0:4]
}
return s
},
"fmtDateTime": func(dateStr, timeStr string) string {
if dateStr == "" {
return ""
}
dateLayouts := []string{
"2006-01-02",
"2006-01-02 15:04:05",
"02/01/2006",
"02/01/2006 15:04:05",
time.RFC3339,
}
var datePart time.Time
for _, layout := range dateLayouts {
if t, err := time.ParseInLocation(layout, dateStr, time.Local); err == nil {
datePart = t
break
}
}
if datePart.IsZero() {
if len(dateStr) >= 10 {
dateStr = dateStr[8:10] + "/" + dateStr[5:7] + "/" + dateStr[0:4]
}
} else {
dateStr = datePart.Format("02/01/2006")
}
if timeStr == "" {
return dateStr
}
timeLayouts := []string{"15:04:05", "15:04"}
var timePart string
for _, layout := range timeLayouts {
if t, err := time.Parse(layout, timeStr); err == nil {
timePart = t.Format("15:04:05")
break
}
}
if timePart == "" {
timePart = timeStr
}
return dateStr + " " + timePart
},
"initials": func(name string) string {
parts := strings.Fields(name)
if len(parts) == 0 {
return "?"
}
if len(parts) == 1 {
return strings.ToUpper(string([]rune(parts[0])[:1]))
}
return strings.ToUpper(string([]rune(parts[0])[:1]) + string([]rune(parts[len(parts)-1])[:1]))
},
"slice": func(args ...int) []int { return args },
"seq": func(n int) []int {
s := make([]int, n)
for i := range s {
s[i] = i
}
return s
},
}
bp := cfg.BasePath // e.g. "/cpone-dashboard" or ""
pageFuncs["b"] = func(path string) string { return bp + path }
dashboard.SetTemplateFuncs(pageFuncs)
newPageTmpl := func(files ...string) *template.Template {
paths := append([]string{"templates/layout/base.html"}, files...)
return template.Must(template.New("").Funcs(pageFuncs).ParseFS(templateFS, paths...))
}
// Propagate basePath to all packages that redirect
auth.SetBasePath(bp)
dashboard.SetBasePath(bp)
arrival.SetBasePath(bp)
progress.SetBasePath(bp)
abnormal.SetBasePath(bp)
result.SetBasePath(bp)
projects.SetBasePath(bp)
// Dashboard pakai templateFS langsung (parse per-handler)
dashboard.SetTemplateFS(&templateFS)
arrival.SetTemplates(newPageTmpl("templates/arrival/index.html"))
progress.SetTemplates(newPageTmpl("templates/progress/index.html"))
abnormal.SetTemplates(newPageTmpl("templates/abnormal/index.html"))
result.SetTemplates(newPageTmpl("templates/result/index.html"))
result.SetPDFBaseURL(cfg.PDFBaseURL)
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Static files — always mounted at bp+/static/
staticSub, _ := fs.Sub(staticFS, "static")
staticPrefix := bp + "/static"
r.Handle(staticPrefix+"/*", http.StripPrefix(staticPrefix+"/", http.FileServer(http.FS(staticSub))))
registerRoutes := func(r chi.Router) {
auth.Routes(r)
r.Group(func(r chi.Router) {
r.Use(auth.Require(cfg.AuthSecret))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, bp+"/projects", http.StatusFound)
})
auth.ProtectedRoutes(r)
r.Route("/projects", func(r chi.Router) { projects.Routes(r) })
r.Route("/dashboard", func(r chi.Router) { dashboard.Routes(r) })
r.Route("/arrival", func(r chi.Router) { arrival.Routes(r) })
r.Route("/progress", func(r chi.Router) { progress.Routes(r) })
r.Route("/abnormal", func(r chi.Router) { abnormal.Routes(r) })
r.Route("/result", func(r chi.Router) { result.Routes(r) })
})
}
if bp == "" {
registerRoutes(r)
} else {
r.Route(bp, registerRoutes)
// redirect bare /cpone-dashboard → /cpone-dashboard/
r.Get(bp, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, bp+"/", http.StatusMovedPermanently)
})
}
log.Printf("server running on :%s (base path: %q)", cfg.AppPort, bp)
if err := http.ListenAndServe(":"+cfg.AppPort, r); err != nil {
log.Fatal(err)
}
}