nidus-sync/htmlpage/sync/page.go
Eli Ribble 9774452821 Switch main report page to better example
This is still pulling from our generic district mock, but it's still
better than what we had. This also includes adding static content
hosting for the bootstrap content on the public domain.
2026-01-07 20:47:55 +00:00

355 lines
12 KiB
Go

package sync
import (
"embed"
"github.com/Gleipnir-Technology/nidus-sync/htmlpage"
//"bytes"
"context"
//"errors"
"fmt"
//"html/template"
//"io"
//"math"
"net/http"
//"os"
//"strconv"
//"strings"
"time"
"github.com/Gleipnir-Technology/nidus-sync/background"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
//"github.com/aarondl/opt/null"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/bob/dialect/psql/sm"
"github.com/uber/h3-go/v4"
)
//go:embed template/*
var embeddedFiles embed.FS
//go:embed static/*
var EmbeddedStaticFS embed.FS
// Authenticated pages
var (
cell = buildTemplate("cell", "authenticated")
dashboard = buildTemplate("dashboard", "authenticated")
oauthPrompt = buildTemplate("oauth-prompt", "authenticated")
settings = buildTemplate("settings", "authenticated")
source = buildTemplate("source", "authenticated")
)
// Unauthenticated pages
var (
admin = buildTemplate("admin", "base")
dataEntry = buildTemplate("data-entry", "base")
dataEntryGood = buildTemplate("data-entry-good", "base")
dataEntryBad = buildTemplate("data-entry-bad", "base")
dispatch = buildTemplate("dispatch", "base")
dispatchResults = buildTemplate("dispatch-results", "base")
mockRoot = buildTemplate("mock-root", "base")
reportPage = buildTemplate("report", "base")
reportConfirmation = buildTemplate("report-confirmation", "base")
reportContribute = buildTemplate("report-contribute", "base")
reportDetail = buildTemplate("report-detail", "base")
reportEvidence = buildTemplate("report-evidence", "base")
reportSchedule = buildTemplate("report-schedule", "base")
reportUpdate = buildTemplate("report-update", "base")
serviceRequest = buildTemplate("service-request", "base")
serviceRequestDetail = buildTemplate("service-request-detail", "base")
serviceRequestLocation = buildTemplate("service-request-location", "base")
serviceRequestMosquito = buildTemplate("service-request-mosquito", "base")
serviceRequestPool = buildTemplate("service-request-pool", "base")
serviceRequestQuick = buildTemplate("service-request-quick", "base")
serviceRequestQuickConfirmation = buildTemplate("service-request-quick-confirmation", "base")
serviceRequestUpdates = buildTemplate("service-request-updates", "base")
settingRoot = buildTemplate("setting-mock", "base")
settingIntegration = buildTemplate("setting-integration", "base")
settingPesticide = buildTemplate("setting-pesticide", "base")
settingPesticideAdd = buildTemplate("setting-pesticide-add", "base")
settingUsers = buildTemplate("setting-user", "base")
settingUsersAdd = buildTemplate("setting-user-add", "base")
signin = buildTemplate("signin", "base")
signup = buildTemplate("signup", "base")
)
var components = [...]string{"header", "map"}
func buildTemplate(files ...string) *htmlpage.BuiltTemplate {
subdir := "htmlpage/sync"
full_files := make([]string, 0)
for _, f := range files {
full_files = append(full_files, fmt.Sprintf("%s/template/%s.html", subdir, f))
}
for _, c := range components {
full_files = append(full_files, fmt.Sprintf("%s/template/components/%s.html", subdir, c))
}
return htmlpage.NewBuiltTemplate(embeddedFiles, "htmlpage/sync/", full_files...)
}
func Cell(ctx context.Context, w http.ResponseWriter, user *models.User, c int64) {
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
return
}
userContent, err := contentForUser(ctx, user)
if err != nil {
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
center, err := h3.Cell(c).LatLng()
if err != nil {
respondError(w, "Failed to get center", err, http.StatusInternalServerError)
return
}
boundary, err := h3.Cell(c).Boundary()
if err != nil {
respondError(w, "Failed to get boundary", err, http.StatusInternalServerError)
return
}
inspections, err := inspectionsByCell(ctx, org, h3.Cell(c))
if err != nil {
respondError(w, "Failed to get inspections by cell", err, http.StatusInternalServerError)
return
}
geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)})
if err != nil {
respondError(w, "Failed to get boundaries", err, http.StatusInternalServerError)
return
}
resolution := h3.Cell(c).Resolution()
sources, err := breedingSourcesByCell(ctx, org, h3.Cell(c))
if err != nil {
respondError(w, "Failed to get sources", err, http.StatusInternalServerError)
return
}
treatments, err := treatmentsByCell(ctx, org, h3.Cell(c))
if err != nil {
respondError(w, "Failed to get treatments", err, http.StatusInternalServerError)
return
}
data := ContentCell{
BreedingSources: sources,
CellBoundary: boundary,
Inspections: inspections,
MapData: ComponentMap{
Center: h3.LatLng{
Lat: center.Lat,
Lng: center.Lng,
},
GeoJSON: geojson,
MapboxToken: config.MapboxToken,
Zoom: resolution + 5,
},
Treatments: treatments,
User: userContent,
}
htmlpage.RenderOrError(w, cell, &data)
}
func Dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
return
}
var lastSync *time.Time
sync, err := org.FieldseekerSyncs(sm.OrderBy("created").Desc()).One(ctx, db.PGInstance.BobDB)
if err != nil {
if err.Error() != "sql: no rows in result set" {
respondError(w, "Failed to get syncs", err, http.StatusInternalServerError)
return
}
} else {
lastSync = &sync.Created
}
is_syncing := background.IsSyncOngoing(org.ID)
inspectionCount, err := org.Mosquitoinspections().Count(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get inspection count", err, http.StatusInternalServerError)
return
}
sourceCount, err := org.Pointlocations().Count(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get source count", err, http.StatusInternalServerError)
return
}
serviceCount, err := org.Servicerequests().Count(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get service count", err, http.StatusInternalServerError)
return
}
recentRequests, err := org.Servicerequests(sm.OrderBy("creationdate").Desc(), sm.Limit(10)).All(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get recent service", err, http.StatusInternalServerError)
return
}
requests := make([]ServiceRequestSummary, 0)
for _, r := range recentRequests {
requests = append(requests, ServiceRequestSummary{
Date: r.Creationdate.MustGet(),
Location: r.Reqaddr1.MustGet(),
Status: "Completed",
})
}
userContent, err := contentForUser(ctx, user)
if err != nil {
respondError(w, "Failed to get user context", err, http.StatusInternalServerError)
return
}
data := ContentDashboard{
CountInspections: int(inspectionCount),
CountMosquitoSources: int(sourceCount),
CountServiceRequests: int(serviceCount),
IsSyncOngoing: is_syncing,
LastSync: lastSync,
MapData: ComponentMap{
MapboxToken: config.MapboxToken,
},
Org: org.Name.MustGet(),
RecentRequests: requests,
User: userContent,
}
htmlpage.RenderOrError(w, dashboard, data)
}
func Mock(t string, w http.ResponseWriter, code string) {
data := ContentMock{
DistrictName: "Delta MVCD",
URLs: ContentMockURLs{
Dispatch: "/mock/dispatch",
DispatchResults: "/mock/dispatch-results",
ReportConfirmation: fmt.Sprintf("/mock/report/%s/confirm", code),
ReportDetail: fmt.Sprintf("/mock/report/%s", code),
ReportContribute: fmt.Sprintf("/mock/report/%s/contribute", code),
ReportEvidence: fmt.Sprintf("/mock/report/%s/evidence", code),
ReportSchedule: fmt.Sprintf("/mock/report/%s/schedule", code),
ReportUpdate: fmt.Sprintf("/mock/report/%s/update", code),
Root: "/mock",
Setting: "/mock/setting",
SettingIntegration: "/mock/setting/integration",
SettingPesticide: "/mock/setting/pesticide",
SettingPesticideAdd: "/mock/setting/pesticide/add",
SettingUser: "/mock/setting/user",
SettingUserAdd: "/mock/setting/user/add",
},
}
template, ok := htmlpage.TemplatesByFilename[t+".html"]
if !ok {
log.Error().Str("template", t).Msg("Failed to find template")
respondError(w, "Failed to render template", nil, http.StatusInternalServerError)
return
}
htmlpage.RenderOrError(w, &template, data)
}
func OauthPrompt(w http.ResponseWriter, user *models.User) {
dp := user.DisplayName
data := ContentDashboard{
User: User{
DisplayName: dp,
Initials: extractInitials(dp),
Username: user.Username,
},
}
htmlpage.RenderOrError(w, oauthPrompt, data)
}
func Settings(w http.ResponseWriter, r *http.Request, user *models.User) {
userContent, err := contentForUser(r.Context(), user)
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
data := ContentAuthenticatedPlaceholder{
User: userContent,
}
htmlpage.RenderOrError(w, settings, data)
}
func Signin(w http.ResponseWriter, errorCode string) {
data := ContentSignin{
InvalidCredentials: errorCode == "invalid-credentials",
}
htmlpage.RenderOrError(w, signin, data)
}
func Signup(w http.ResponseWriter, path string) {
data := ContentSignup{}
htmlpage.RenderOrError(w, signup, data)
}
func Source(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUID) {
org, err := user.Organization().One(r.Context(), db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
return
}
userContent, err := contentForUser(r.Context(), user)
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
s, err := sourceByGlobalId(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get source", err, http.StatusInternalServerError)
return
}
inspections, err := inspectionsBySource(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get inspections", err, http.StatusInternalServerError)
return
}
traps, err := trapsBySource(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get traps", err, http.StatusInternalServerError)
return
}
treatments, err := treatmentsBySource(r.Context(), org, id)
if err != nil {
respondError(w, "Failed to get treatments", err, http.StatusInternalServerError)
return
}
treatment_models := modelTreatment(treatments)
latlng, err := s.H3Cell.LatLng()
if err != nil {
respondError(w, "Failed to get latlng", err, http.StatusInternalServerError)
return
}
data := ContentSource{
Inspections: inspections,
MapData: ComponentMap{
Center: latlng,
//GeoJSON:
MapboxToken: config.MapboxToken,
Markers: []MapMarker{
MapMarker{
LatLng: latlng,
},
},
Zoom: 13,
},
Source: s,
Traps: traps,
Treatments: treatments,
TreatmentModels: treatment_models,
User: userContent,
}
htmlpage.RenderOrError(w, source, data)
}
// Respond with an error that is visible to the user
func respondError(w http.ResponseWriter, m string, e error, s int) {
log.Warn().Int("status", s).Err(e).Str("user message", m).Msg("Responding with an error from sync pages")
http.Error(w, m, s)
}