311 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|