diff --git a/endpoint.go b/endpoint.go
index 7ba8c8f8..7020ad18 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -13,7 +13,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/background"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
- "github.com/Gleipnir-Technology/nidus-sync/htmlpage"
+ "github.com/Gleipnir-Technology/nidus-sync/htmlpage/sync"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
@@ -56,7 +56,7 @@ func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) {
respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest)
return
}
- htmlpage.Cell(r.Context(), w, user, cell)
+ sync.Cell(r.Context(), w, user, cell)
}
func getFavicon(w http.ResponseWriter, r *http.Request) {
@@ -71,7 +71,7 @@ func getOAuthRefresh(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/?next=/oauth/refresh", http.StatusFound)
return
}
- htmlpage.OauthPrompt(w, user)
+ sync.OauthPrompt(w, user)
}
func getQRCodeReport(w http.ResponseWriter, r *http.Request) {
@@ -148,7 +148,7 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
}
if user == nil {
errorCode := r.URL.Query().Get("error")
- htmlpage.Signin(w, errorCode)
+ sync.Signin(w, errorCode)
return
} else {
has, err := background.HasFieldseekerConnection(r.Context(), user)
@@ -157,10 +157,10 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
return
}
if has {
- htmlpage.Dashboard(r.Context(), w, user)
+ sync.Dashboard(r.Context(), w, user)
return
} else {
- htmlpage.OauthPrompt(w, user)
+ sync.OauthPrompt(w, user)
return
}
}
@@ -170,16 +170,16 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
}
func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) {
- htmlpage.Settings(w, r, u)
+ sync.Settings(w, r, u)
}
func getSignin(w http.ResponseWriter, r *http.Request) {
errorCode := r.URL.Query().Get("error")
- htmlpage.Signin(w, errorCode)
+ sync.Signin(w, errorCode)
}
func getSignup(w http.ResponseWriter, r *http.Request) {
- htmlpage.Signup(w, r.URL.Path)
+ sync.Signup(w, r.URL.Path)
}
func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
@@ -193,7 +193,7 @@ func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest)
return
}
- htmlpage.Source(w, r, u, globalid)
+ sync.Source(w, r, u, globalid)
}
func postSMS(w http.ResponseWriter, r *http.Request) {
@@ -327,6 +327,6 @@ func renderMock(templateName string) http.HandlerFunc {
if code == "" {
code = "abc-123"
}
- htmlpage.Mock(templateName, w, code)
+ sync.Mock(templateName, w, code)
}
}
diff --git a/htmlpage/h3.go b/htmlpage/h3.go
new file mode 100644
index 00000000..6de2b817
--- /dev/null
+++ b/htmlpage/h3.go
@@ -0,0 +1,39 @@
+package htmlpage
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/uber/h3-go/v4"
+)
+
+func gisStatement(cb h3.CellBoundary) string {
+ var content strings.Builder
+ for i, p := range cb {
+ if i != 0 {
+ content.WriteString(", ")
+ }
+ content.WriteString(fmt.Sprintf("%f %f", p.Lng, p.Lat))
+ }
+ // Repeat the first coordinate to close the polygon
+ content.WriteString(fmt.Sprintf(", %f %f", cb[0].Lng, cb[0].Lat))
+ return fmt.Sprintf("ST_GeomFromText('POLYGON((%s))', 3857)", content.String())
+}
+
+func latLngDisplay(ll h3.LatLng) string {
+ latDir := "N"
+ latVal := ll.Lat
+ if ll.Lat < 0 {
+ latDir = "S"
+ latVal = -ll.Lat
+ }
+
+ lngDir := "E"
+ lngVal := ll.Lng
+ if ll.Lng < 0 {
+ lngDir = "W"
+ lngVal = -ll.Lng
+ }
+
+ return fmt.Sprintf("%.4f° %s, %.4f° %s", latVal, latDir, lngVal, lngDir)
+}
diff --git a/htmlpage/html.go b/htmlpage/html.go
index 39f66c19..29831177 100644
--- a/htmlpage/html.go
+++ b/htmlpage/html.go
@@ -2,7 +2,6 @@ package htmlpage
import (
"bytes"
- "context"
"embed"
"errors"
"fmt"
@@ -11,194 +10,30 @@ import (
"math"
"net/http"
"os"
+ "path"
"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/db/sql"
- "github.com/Gleipnir-Technology/nidus-sync/h3utils"
- "github.com/Gleipnir-Technology/nidus-sync/notification"
"github.com/aarondl/opt/null"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
- "github.com/stephenafamo/bob"
- "github.com/stephenafamo/bob/dialect/psql"
- "github.com/stephenafamo/bob/dialect/psql/sm"
- "github.com/uber/h3-go/v4"
)
-//go:embed templates/*
-var embeddedFiles embed.FS
-
-// Authenticated pages
-var (
- cell = newBuiltTemplate("cell", "authenticated")
- dashboard = newBuiltTemplate("dashboard", "authenticated")
- oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
- settings = newBuiltTemplate("settings", "authenticated")
- source = newBuiltTemplate("source", "authenticated")
-)
-
-// Unauthenticated pages
-var (
- admin = newBuiltTemplate("admin", "base")
- dataEntry = newBuiltTemplate("data-entry", "base")
- dataEntryGood = newBuiltTemplate("data-entry-good", "base")
- dataEntryBad = newBuiltTemplate("data-entry-bad", "base")
- dispatch = newBuiltTemplate("dispatch", "base")
- dispatchResults = newBuiltTemplate("dispatch-results", "base")
- mockRoot = newBuiltTemplate("mock-root", "base")
- reportPage = newBuiltTemplate("report", "base")
- reportConfirmation = newBuiltTemplate("report-confirmation", "base")
- reportContribute = newBuiltTemplate("report-contribute", "base")
- reportDetail = newBuiltTemplate("report-detail", "base")
- reportEvidence = newBuiltTemplate("report-evidence", "base")
- reportSchedule = newBuiltTemplate("report-schedule", "base")
- reportUpdate = newBuiltTemplate("report-update", "base")
- serviceRequest = newBuiltTemplate("service-request", "base")
- serviceRequestDetail = newBuiltTemplate("service-request-detail", "base")
- serviceRequestLocation = newBuiltTemplate("service-request-location", "base")
- serviceRequestMosquito = newBuiltTemplate("service-request-mosquito", "base")
- serviceRequestPool = newBuiltTemplate("service-request-pool", "base")
- serviceRequestQuick = newBuiltTemplate("service-request-quick", "base")
- serviceRequestQuickConfirmation = newBuiltTemplate("service-request-quick-confirmation", "base")
- serviceRequestUpdates = newBuiltTemplate("service-request-updates", "base")
- settingRoot = newBuiltTemplate("setting-mock", "base")
- settingIntegration = newBuiltTemplate("setting-integration", "base")
- settingPesticide = newBuiltTemplate("setting-pesticide", "base")
- settingPesticideAdd = newBuiltTemplate("setting-pesticide-add", "base")
- settingUsers = newBuiltTemplate("setting-user", "base")
- settingUsersAdd = newBuiltTemplate("setting-user-add", "base")
- signin = newBuiltTemplate("signin", "base")
- signup = newBuiltTemplate("signup", "base")
-)
-var components = [...]string{"header", "map"}
-var templatesByFilename = make(map[string]BuiltTemplate, 0)
-
-type BreedingSourceSummary struct {
- ID uuid.UUID
- Type string
- LastInspected *time.Time
- LastTreated *time.Time
-}
+var TemplatesByFilename = make(map[string]BuiltTemplate, 0)
type BuiltTemplate struct {
- files []string
- name string
+ files []string
+ subdir string
+ // Nil if we are going to read templates off disk every time we render
+ // because we are in development mode.
template *template.Template
}
-type MapMarker struct {
- LatLng h3.LatLng
-}
-type ComponentMap struct {
- Center h3.LatLng
- GeoJSON interface{}
- MapboxToken string
- Markers []MapMarker
- Zoom int
-}
-type ContentAuthenticatedPlaceholder struct {
- User User
-}
-type ContentCell struct {
- BreedingSources []BreedingSourceSummary
- CellBoundary h3.CellBoundary
- Inspections []Inspection
- MapData ComponentMap
- Treatments []Treatment
- User User
-}
-type ContentMockURLs struct {
- Dispatch string
- DispatchResults string
- ReportConfirmation string
- ReportDetail string
- ReportContribute string
- ReportEvidence string
- ReportSchedule string
- ReportUpdate string
- Root string
- Setting string
- SettingIntegration string
- SettingPesticide string
- SettingPesticideAdd string
- SettingUser string
- SettingUserAdd string
-}
-type ContentMock struct {
- DistrictName string
- URLs ContentMockURLs
-}
-type ContentReportDetail struct {
- NextURL string
- UpdateURL string
-}
-type ContentReportDiagnostic struct {
-}
-type ContentDashboard struct {
- CountInspections int
- CountMosquitoSources int
- CountServiceRequests int
- Geo template.JS
- IsSyncOngoing bool
- LastSync *time.Time
- MapData ComponentMap
- Org string
- RecentRequests []ServiceRequestSummary
- User User
-}
-
-type ContentDashboardLoading struct {
- User User
-}
-
-type ContentPlaceholder struct {
-}
-type ContentSignin struct {
- InvalidCredentials bool
-}
-type ContentSignup struct{}
-type ContentSource struct {
- Inspections []Inspection
- MapData ComponentMap
- Source *BreedingSourceDetail
- Traps []TrapNearby
- Treatments []Treatment
- //TreatmentCadence TreatmentCadence
- TreatmentModels []TreatmentModel
- User User
-}
-type Inspection struct {
- Action string
- Date *time.Time
- Notes string
- Location string
- LocationID uuid.UUID
-}
-type Link struct {
- Href string
- Title string
-}
-type ServiceRequestSummary struct {
- Date time.Time
- Location string
- Status string
-}
-type User struct {
- DisplayName string
- Initials string
- Notifications []notification.Notification
- Username string
-}
-
-func (bt *BuiltTemplate) ExecuteTemplate(w io.Writer, data any) error {
- name := bt.files[0] + ".html"
+func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error {
if bt.template == nil {
+ name := path.Base(bt.files[0])
templ, err := parseFromDisk(bt.files)
if err != nil {
return fmt.Errorf("Failed to parse template file: %w", err)
@@ -207,12 +42,55 @@ func (bt *BuiltTemplate) ExecuteTemplate(w io.Writer, data any) error {
w.Write([]byte("Failed to read from disk: "))
return errors.New("Template parsing failed")
}
+ log.Debug().Str("name", templ.Name()).Msg("Parsed template")
return templ.ExecuteTemplate(w, name, data)
} else {
+ name := path.Base(bt.files[0])
return bt.template.ExecuteTemplate(w, name, data)
}
}
+func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, files ...string) *BuiltTemplate {
+ files_on_disk := true
+ for _, f := range files {
+ _, err := os.Stat(f)
+ if err != nil {
+ files_on_disk = false
+ if !config.IsProductionEnvironment() {
+ log.Warn().Str("file", f).Msg("template file is not on disk")
+ }
+ break
+ }
+ }
+ var result BuiltTemplate
+ if files_on_disk {
+ result = BuiltTemplate{
+ files: files,
+ subdir: subdir,
+ template: nil,
+ }
+ } else {
+ result = BuiltTemplate{
+ files: files,
+ subdir: subdir,
+ template: parseEmbedded(embeddedFiles, subdir, files),
+ }
+ }
+ TemplatesByFilename[files[0]] = result
+ return &result
+}
+
+func RenderOrError(w http.ResponseWriter, template *BuiltTemplate, context interface{}) {
+ buf := &bytes.Buffer{}
+ err := template.executeTemplate(buf, context)
+ if err != nil {
+ log.Error().Err(err).Strs("files", template.files).Msg("Failed to render template")
+ respondError(w, "Failed to render template", err, http.StatusInternalServerError)
+ return
+ }
+ buf.WriteTo(w)
+}
+
func bigNumber(n int) string {
// Convert the number to a string
numStr := strconv.FormatInt(int64(n), 10)
@@ -229,318 +107,6 @@ func bigNumber(n int) string {
return result.String()
}
-func contentForUser(ctx context.Context, user *models.User) (User, error) {
- notifications, err := notification.ForUser(ctx, user)
- if err != nil {
- return User{}, err
- }
- return User{
- DisplayName: user.DisplayName,
- Initials: extractInitials(user.DisplayName),
- Notifications: notifications,
- Username: user.Username,
- }, nil
-
-}
-func extractInitials(name string) string {
- parts := strings.Fields(name)
- var initials strings.Builder
-
- for _, part := range parts {
- if len(part) > 0 {
- initials.WriteString(strings.ToUpper(string(part[0])))
- }
- }
-
- return initials.String()
-}
-
-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,
- }
- 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,
- }
- 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 := templatesByFilename[t]
- if !ok {
- log.Error().Str("template", t).Msg("Failed to find template")
- respondError(w, "Failed to render template", nil, http.StatusInternalServerError)
- return
- }
- 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,
- },
- }
- 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,
- }
- renderOrError(w, settings, data)
-}
-
-func Signin(w http.ResponseWriter, errorCode string) {
- data := ContentSignin{
- InvalidCredentials: errorCode == "invalid-credentials",
- }
- renderOrError(w, signin, data)
-}
-
-func Signup(w http.ResponseWriter, path string) {
- data := ContentSignup{}
- 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,
- }
-
- renderOrError(w, source, data)
-}
-
-func gisStatement(cb h3.CellBoundary) string {
- var content strings.Builder
- for i, p := range cb {
- if i != 0 {
- content.WriteString(", ")
- }
- content.WriteString(fmt.Sprintf("%f %f", p.Lng, p.Lat))
- }
- // Repeat the first coordinate to close the polygon
- content.WriteString(fmt.Sprintf(", %f %f", cb[0].Lng, cb[0].Lat))
- return fmt.Sprintf("ST_GeomFromText('POLYGON((%s))', 3857)", content.String())
-}
-
-func latLngDisplay(ll h3.LatLng) string {
- latDir := "N"
- latVal := ll.Lat
- if ll.Lat < 0 {
- latDir = "S"
- latVal = -ll.Lat
- }
-
- lngDir := "E"
- lngVal := ll.Lng
- if ll.Lng < 0 {
- lngDir = "W"
- lngVal = -ll.Lng
- }
-
- return fmt.Sprintf("%.4f° %s, %.4f° %s", latVal, latDir, lngVal, lngDir)
-}
-
func makeFuncMap() template.FuncMap {
funcMap := template.FuncMap{
"bigNumber": bigNumber,
@@ -555,63 +121,26 @@ func makeFuncMap() template.FuncMap {
}
return funcMap
}
-func newBuiltTemplate(name string, files ...string) *BuiltTemplate {
- files_on_disk := true
- all_files := append([]string{name}, files...)
- for _, f := range all_files {
- full_path := "templates/" + f + ".html"
- _, err := os.Stat(full_path)
- if err != nil {
- files_on_disk = false
- break
- }
- }
- var result BuiltTemplate
- if files_on_disk {
- result = BuiltTemplate{
- files: all_files,
- name: name,
- template: nil,
- }
- } else {
- result = BuiltTemplate{
- files: all_files,
- name: name,
- template: parseEmbedded(all_files),
- }
- }
- templatesByFilename[name] = result
- return &result
-}
-
-func parseEmbedded(files []string) *template.Template {
+func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template {
funcMap := makeFuncMap()
// Remap the file names to embedded paths
- paths := make([]string, 0)
+ embeddedFilePaths := make([]string, 0)
for _, f := range files {
- paths = append(paths, "templates/"+f+".html")
+ embeddedFilePaths = append(embeddedFilePaths, strings.TrimPrefix(f, subdir))
}
- for _, f := range components {
- paths = append(paths, "templates/components/"+f+".html")
- }
- name := files[0]
+ name := path.Base(embeddedFilePaths[0])
+ log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
return template.Must(
- template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, paths...))
+ template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...))
}
func parseFromDisk(files []string) (*template.Template, error) {
funcMap := makeFuncMap()
- paths := make([]string, 0)
- for _, f := range files {
- paths = append(paths, "templates/"+f+".html")
- }
- name := files[0] + ".html"
- for _, f := range components {
- paths = append(paths, "templates/components/"+f+".html")
- }
- templ, err := template.New(name).Funcs(funcMap).ParseFiles(paths...)
+ name := path.Base(files[0])
+ log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
+ templ, err := template.New(name).Funcs(funcMap).ParseFiles(files...)
if err != nil {
- return nil, fmt.Errorf("Failed to parse %s: %w", paths, err)
+ return nil, fmt.Errorf("Failed to parse %s: %w", files, err)
}
return templ, nil
}
@@ -750,203 +279,6 @@ func timeSince(t *time.Time) string {
return fmt.Sprintf("%d days ago", int(days))
}
}
-
-func trapsBySource(ctx context.Context, org *models.Organization, sourceID uuid.UUID) ([]TrapNearby, error) {
- locations, err := sql.TrapLocationBySourceID(org.ID, sourceID).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return nil, fmt.Errorf("Failed to query rows: %w", err)
- }
-
- location_ids := make([]uuid.UUID, 0)
- var args []bob.Expression
- for _, location := range locations {
- location_ids = append(location_ids, location.TrapLocationGlobalid)
- args = append(args, psql.Arg(location.TrapLocationGlobalid))
- }
- /*
- trap_data, err := org.FSTrapdata(
- sm.Where(
- models.FSTrapdata.Columns.LocID.In(args...),
- ),
- sm.OrderBy("enddatetime"),
- ).All(ctx, db.PGInstance.BobDB)
- */
-
- /*
- query := org.FSTrapdata(
- sm.From(
- psql.Select(
- sm.From(psql.F("ROW_NUMBER")(
- fm.Over(
- wm.PartitionBy(models.FSTrapdata.Columns.LocID),
- wm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
- ),
- )).As("row_num"),
- sm.Where(models.FSTrapdata.Columns.LocID.In(args...))),
- ),
- sm.Where(psql.Quote("row_num").LTE(psql.Arg(10))),
- sm.OrderBy(models.FSTrapdata.Columns.LocID),
- sm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
- )
- */
- /*
- query := psql.Select(
- sm.From(
- psql.Select(
- sm.Columns(
- models.FSTrapdata.Columns.Globalid,
- psql.F("ROW_NUMBER")(
- fm.Over(
- wm.PartitionBy(models.FSTrapdata.Columns.LocID),
- wm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
- ),
- ).As("row_num"),
- sm.From(models.FSTrapdata.Name()),
- ),
- sm.Where(models.FSTrapdata.Columns.LocID.In(args...))),
- ),
- sm.Where(psql.Quote("row_num").LTE(psql.Arg(10))),
- sm.OrderBy(models.FSTrapdata.Columns.LocID),
- sm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
- )
- log.Info().Str("trapdata", queryToString(query)).Msg("Getting trap data")
- trap_data, err := query.Exec(ctx, db.PGInstance.BobDB)
- */
-
- trap_data, err := sql.TrapDataByLocationIDRecent(org.ID, location_ids).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return nil, fmt.Errorf("Failed to query trap data: %w", err)
- }
-
- counts, err := sql.TrapCountByLocationID(org.ID, location_ids).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return nil, fmt.Errorf("Failed to query trap counts: %w", err)
- }
-
- traps, err := toTemplateTraps(locations, trap_data, counts)
- if err != nil {
- return nil, fmt.Errorf("Failed to convert trap data: %w", err)
- }
- return traps, nil
-}
-
-func renderOrError(w http.ResponseWriter, template *BuiltTemplate, context interface{}) {
- buf := &bytes.Buffer{}
- err := template.ExecuteTemplate(buf, context)
- if err != nil {
- log.Error().Err(err).Str("template", template.name).Msg("Failed to render template")
- respondError(w, "Failed to render template", err, http.StatusInternalServerError)
- return
- }
- buf.WriteTo(w)
-}
-
-func treatmentsBySource(ctx context.Context, org *models.Organization, sourceID uuid.UUID) ([]Treatment, error) {
- var results []Treatment
- rows, err := org.Treatments(
- sm.Where(
- models.FieldseekerTreatments.Columns.Pointlocid.EQ(psql.Arg(sourceID)),
- ),
- sm.OrderBy("enddatetime").Desc(),
- ).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return results, fmt.Errorf("Failed to query rows: %w", err)
- }
- //log.Info().Int("row count", len(rows)).Msg("Getting treatments")
- return toTemplateTreatment(rows)
-}
-
-func treatmentsByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]Treatment, error) {
- var results []Treatment
- boundary, err := c.Boundary()
- if err != nil {
- return results, fmt.Errorf("Failed to get cell boundary: %w", err)
- }
- geom_query := gisStatement(boundary)
- rows, err := org.Treatments(
- sm.Where(
- psql.F("ST_Within", "geospatial", geom_query),
- ),
- sm.OrderBy("pointlocid"),
- sm.OrderBy("enddatetime"),
- ).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return results, fmt.Errorf("Failed to query rows: %w", err)
- }
- return toTemplateTreatment(rows)
-}
-func inspectionsByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]Inspection, error) {
- var results []Inspection
-
- boundary, err := c.Boundary()
- if err != nil {
- return results, fmt.Errorf("Failed to get cell boundary: %w", err)
- }
- geom_query := gisStatement(boundary)
- rows, err := org.Mosquitoinspections(
- sm.Where(
- psql.F("ST_Within", "geospatial", geom_query),
- ),
- sm.OrderBy("pointlocid"),
- sm.OrderBy("enddatetime"),
- ).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return results, fmt.Errorf("Failed to query rows: %w", err)
- }
- return toTemplateInspection(rows)
-}
-func inspectionsBySource(ctx context.Context, org *models.Organization, sourceID uuid.UUID) ([]Inspection, error) {
- var results []Inspection
-
- rows, err := org.Mosquitoinspections(
- sm.Where(
- models.FieldseekerMosquitoinspections.Columns.Pointlocid.EQ(psql.Arg(sourceID)),
- ),
- sm.OrderBy("enddatetime").Desc(),
- ).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return results, fmt.Errorf("Failed to query rows: %w", err)
- }
- return toTemplateInspection(rows)
-}
-func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSourceSummary, error) {
- var results []BreedingSourceSummary
-
- boundary, err := c.Boundary()
- if err != nil {
- return results, fmt.Errorf("Failed to get cell boundary: %w", err)
- }
- geom_query := gisStatement(boundary)
- rows, err := org.Pointlocations(
- sm.Where(
- psql.F("ST_Within", "geospatial", geom_query),
- ),
- sm.OrderBy("lasttreatdate"),
- ).All(ctx, db.PGInstance.BobDB)
- if err != nil {
- return results, fmt.Errorf("Failed to query rows: %w", err)
- }
- for _, r := range rows {
- var last_inspected *time.Time
- if !r.Lastinspectdate.IsNull() {
- l := r.Lastinspectdate.MustGet()
- last_inspected = &l
- }
- var last_treat_date *time.Time
- if !r.Lasttreatdate.IsNull() {
- l := r.Lasttreatdate.MustGet()
- last_treat_date = &l
- }
- results = append(results, BreedingSourceSummary{
- ID: r.Globalid,
- LastInspected: last_inspected,
- LastTreated: last_treat_date,
- Type: r.Habitat.GetOr("none"),
- })
- }
- return results, nil
-}
-
func uuidShort(uuid uuid.UUID) string {
s := uuid.String()
if len(s) < 7 {
@@ -955,13 +287,3 @@ func uuidShort(uuid uuid.UUID) string {
return s[:3] + "..." + s[len(s)-4:]
}
-
-func sourceByGlobalId(ctx context.Context, org *models.Organization, id uuid.UUID) (*BreedingSourceDetail, error) {
- row, err := org.Pointlocations(
- sm.Where(models.FieldseekerPointlocations.Columns.Globalid.EQ(psql.Arg(id))),
- ).One(ctx, db.PGInstance.BobDB)
- if err != nil {
- return nil, fmt.Errorf("Failed to get point location: %w", err)
- }
- return toTemplateBreedingSource(row), nil
-}
diff --git a/htmlpage/public-reports/page.go b/htmlpage/public-reports/page.go
new file mode 100644
index 00000000..c81b06b1
--- /dev/null
+++ b/htmlpage/public-reports/page.go
@@ -0,0 +1,31 @@
+package publicreports
+
+import (
+ "embed"
+ "fmt"
+
+ "github.com/Gleipnir-Technology/nidus-sync/htmlpage"
+)
+
+//go:embed template/*
+var embeddedFiles embed.FS
+
+type RootContext struct{}
+
+var (
+ Root = buildTemplate("root", "base")
+)
+
+var components = [...]string{}
+
+func buildTemplate(files ...string) *htmlpage.BuiltTemplate {
+ subdir := "htmlpage/public-reports"
+ 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/public-reports/", full_files...)
+}
diff --git a/htmlpage/templates/base.html b/htmlpage/public-reports/template/base.html
similarity index 100%
rename from htmlpage/templates/base.html
rename to htmlpage/public-reports/template/base.html
diff --git a/htmlpage/public-reports/template/root.html b/htmlpage/public-reports/template/root.html
new file mode 100644
index 00000000..dfe9c3ef
--- /dev/null
+++ b/htmlpage/public-reports/template/root.html
@@ -0,0 +1,224 @@
+{{template "base.html" .}}
+
+{{define "title"}}Login{{end}}
+{{define "extraheader"}}
+
+{{end}}
+{{define "content"}}
+
+
+
+
+
+
+
+ This page demonstrates the various ways customers can access the Green Pool Reporting system.
+
+
+
+
+
+
+
Text Message Entry Point
+
+
+
Customers will receive the following text message with a link to begin the reporting process:
+
+
+
Vector Control: We noticed a potential green pool at your property. Please tap the link to report status or schedule inspection:
/report-detail
+
+
+
+
SMS Details:
+
+ Sent via automated system after aerial detection
+ Contains unique tracking link for each property
+ Customers tap link to open mobile browser
+
+
+
+
+
+
+
+
+
Door Hanger QR Code Entry Point
+
+
+
Inspectors will leave door hangers with a QR code for properties where no one is home:
+
+
+
IMPORTANT NOTICE
+
We visited regarding a potential mosquito breeding site.
+
+
+
+
Scan this code with your phone camera to report your pool status or schedule an inspection.
+
Or visit: /report-detail
+
+
+
+
Door Hanger Details:
+
+ Physical notices left on the door handle
+ QR code contains property-specific link
+ Fallback URL provided for manual entry
+
+
+
+
+
+
+
+
+
Email Notification Entry Point
+
+
+
Property owners will receive this email as a follow-up to other communication attempts:
+
+
+
+
+
+
Dear Property Owner,
+
+
Our recent surveillance has identified a potential unmaintained swimming pool at your property located at 123 Main Street . Untreated pools can become mosquito breeding grounds and pose public health risks, including the spread of West Nile virus and other diseases.
+
+
+
+
Please click the button above or visit /report-detail to complete a brief questionnaire about your pool status. This will help us determine if an inspection is needed or if you've already addressed the issue.
+
+
Thank you for helping keep our community safe and healthy.
+
+
Sincerely,
+ Vector Control Department
+ County Health Services
+
+
+
+
+
Email Details:
+
+ Sent as follow-up or for property owners with registered email addresses
+ Contains clear call-to-action button and alternative text link
+ Explains reason for contact and next steps
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/htmlpage/model_conversion.go b/htmlpage/sync/model_conversion.go
similarity index 99%
rename from htmlpage/model_conversion.go
rename to htmlpage/sync/model_conversion.go
index e93f2480..6f07ec8a 100644
--- a/htmlpage/model_conversion.go
+++ b/htmlpage/sync/model_conversion.go
@@ -1,4 +1,4 @@
-package htmlpage
+package sync
import (
"errors"
diff --git a/htmlpage/sync/page.go b/htmlpage/sync/page.go
new file mode 100644
index 00000000..5daefbe7
--- /dev/null
+++ b/htmlpage/sync/page.go
@@ -0,0 +1,352 @@
+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
+
+// 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]
+ 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)
+}
diff --git a/htmlpage/templates/admin.html b/htmlpage/sync/template/admin.html
similarity index 100%
rename from htmlpage/templates/admin.html
rename to htmlpage/sync/template/admin.html
diff --git a/htmlpage/templates/authenticated.html b/htmlpage/sync/template/authenticated.html
similarity index 100%
rename from htmlpage/templates/authenticated.html
rename to htmlpage/sync/template/authenticated.html
diff --git a/htmlpage/sync/template/base.html b/htmlpage/sync/template/base.html
new file mode 100644
index 00000000..f1304fbb
--- /dev/null
+++ b/htmlpage/sync/template/base.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {{template "title" .}} - Nidus Sync
+
+
+
+
+
+
+ {{block "extraheader" .}} {{end}}
+
+
+{{template "content" .}}
+
+
+
diff --git a/htmlpage/templates/cell.html b/htmlpage/sync/template/cell.html
similarity index 100%
rename from htmlpage/templates/cell.html
rename to htmlpage/sync/template/cell.html
diff --git a/htmlpage/templates/components/header.html b/htmlpage/sync/template/components/header.html
similarity index 100%
rename from htmlpage/templates/components/header.html
rename to htmlpage/sync/template/components/header.html
diff --git a/htmlpage/templates/components/map.html b/htmlpage/sync/template/components/map.html
similarity index 100%
rename from htmlpage/templates/components/map.html
rename to htmlpage/sync/template/components/map.html
diff --git a/htmlpage/templates/dashboard.html b/htmlpage/sync/template/dashboard.html
similarity index 100%
rename from htmlpage/templates/dashboard.html
rename to htmlpage/sync/template/dashboard.html
diff --git a/htmlpage/templates/data-entry-bad.html b/htmlpage/sync/template/data-entry-bad.html
similarity index 100%
rename from htmlpage/templates/data-entry-bad.html
rename to htmlpage/sync/template/data-entry-bad.html
diff --git a/htmlpage/templates/data-entry-good.html b/htmlpage/sync/template/data-entry-good.html
similarity index 100%
rename from htmlpage/templates/data-entry-good.html
rename to htmlpage/sync/template/data-entry-good.html
diff --git a/htmlpage/templates/data-entry.html b/htmlpage/sync/template/data-entry.html
similarity index 100%
rename from htmlpage/templates/data-entry.html
rename to htmlpage/sync/template/data-entry.html
diff --git a/htmlpage/templates/dispatch-results.html b/htmlpage/sync/template/dispatch-results.html
similarity index 100%
rename from htmlpage/templates/dispatch-results.html
rename to htmlpage/sync/template/dispatch-results.html
diff --git a/htmlpage/templates/dispatch.html b/htmlpage/sync/template/dispatch.html
similarity index 100%
rename from htmlpage/templates/dispatch.html
rename to htmlpage/sync/template/dispatch.html
diff --git a/htmlpage/templates/empty-auth.html b/htmlpage/sync/template/empty-auth.html
similarity index 100%
rename from htmlpage/templates/empty-auth.html
rename to htmlpage/sync/template/empty-auth.html
diff --git a/htmlpage/templates/empty.html b/htmlpage/sync/template/empty.html
similarity index 100%
rename from htmlpage/templates/empty.html
rename to htmlpage/sync/template/empty.html
diff --git a/htmlpage/templates/mock-root.html b/htmlpage/sync/template/mock-root.html
similarity index 100%
rename from htmlpage/templates/mock-root.html
rename to htmlpage/sync/template/mock-root.html
diff --git a/htmlpage/templates/oauth-prompt.html b/htmlpage/sync/template/oauth-prompt.html
similarity index 100%
rename from htmlpage/templates/oauth-prompt.html
rename to htmlpage/sync/template/oauth-prompt.html
diff --git a/htmlpage/templates/report-confirmation.html b/htmlpage/sync/template/report-confirmation.html
similarity index 100%
rename from htmlpage/templates/report-confirmation.html
rename to htmlpage/sync/template/report-confirmation.html
diff --git a/htmlpage/templates/report-contribute.html b/htmlpage/sync/template/report-contribute.html
similarity index 100%
rename from htmlpage/templates/report-contribute.html
rename to htmlpage/sync/template/report-contribute.html
diff --git a/htmlpage/templates/report-detail.html b/htmlpage/sync/template/report-detail.html
similarity index 100%
rename from htmlpage/templates/report-detail.html
rename to htmlpage/sync/template/report-detail.html
diff --git a/htmlpage/templates/report-evidence.html b/htmlpage/sync/template/report-evidence.html
similarity index 100%
rename from htmlpage/templates/report-evidence.html
rename to htmlpage/sync/template/report-evidence.html
diff --git a/htmlpage/templates/report-schedule.html b/htmlpage/sync/template/report-schedule.html
similarity index 100%
rename from htmlpage/templates/report-schedule.html
rename to htmlpage/sync/template/report-schedule.html
diff --git a/htmlpage/templates/report-update.html b/htmlpage/sync/template/report-update.html
similarity index 100%
rename from htmlpage/templates/report-update.html
rename to htmlpage/sync/template/report-update.html
diff --git a/htmlpage/templates/report.html b/htmlpage/sync/template/report.html
similarity index 100%
rename from htmlpage/templates/report.html
rename to htmlpage/sync/template/report.html
diff --git a/htmlpage/templates/service-request-detail.html b/htmlpage/sync/template/service-request-detail.html
similarity index 100%
rename from htmlpage/templates/service-request-detail.html
rename to htmlpage/sync/template/service-request-detail.html
diff --git a/htmlpage/templates/service-request-location.html b/htmlpage/sync/template/service-request-location.html
similarity index 100%
rename from htmlpage/templates/service-request-location.html
rename to htmlpage/sync/template/service-request-location.html
diff --git a/htmlpage/templates/service-request-mosquito.html b/htmlpage/sync/template/service-request-mosquito.html
similarity index 100%
rename from htmlpage/templates/service-request-mosquito.html
rename to htmlpage/sync/template/service-request-mosquito.html
diff --git a/htmlpage/templates/service-request-pool.html b/htmlpage/sync/template/service-request-pool.html
similarity index 100%
rename from htmlpage/templates/service-request-pool.html
rename to htmlpage/sync/template/service-request-pool.html
diff --git a/htmlpage/templates/service-request-quick-confirmation.html b/htmlpage/sync/template/service-request-quick-confirmation.html
similarity index 100%
rename from htmlpage/templates/service-request-quick-confirmation.html
rename to htmlpage/sync/template/service-request-quick-confirmation.html
diff --git a/htmlpage/templates/service-request-quick.html b/htmlpage/sync/template/service-request-quick.html
similarity index 100%
rename from htmlpage/templates/service-request-quick.html
rename to htmlpage/sync/template/service-request-quick.html
diff --git a/htmlpage/templates/service-request-updates.html b/htmlpage/sync/template/service-request-updates.html
similarity index 100%
rename from htmlpage/templates/service-request-updates.html
rename to htmlpage/sync/template/service-request-updates.html
diff --git a/htmlpage/templates/service-request.html b/htmlpage/sync/template/service-request.html
similarity index 100%
rename from htmlpage/templates/service-request.html
rename to htmlpage/sync/template/service-request.html
diff --git a/htmlpage/templates/setting-integration.html b/htmlpage/sync/template/setting-integration.html
similarity index 100%
rename from htmlpage/templates/setting-integration.html
rename to htmlpage/sync/template/setting-integration.html
diff --git a/htmlpage/templates/setting-mock.html b/htmlpage/sync/template/setting-mock.html
similarity index 100%
rename from htmlpage/templates/setting-mock.html
rename to htmlpage/sync/template/setting-mock.html
diff --git a/htmlpage/templates/setting-pesticide-add.html b/htmlpage/sync/template/setting-pesticide-add.html
similarity index 100%
rename from htmlpage/templates/setting-pesticide-add.html
rename to htmlpage/sync/template/setting-pesticide-add.html
diff --git a/htmlpage/templates/setting-pesticide.html b/htmlpage/sync/template/setting-pesticide.html
similarity index 100%
rename from htmlpage/templates/setting-pesticide.html
rename to htmlpage/sync/template/setting-pesticide.html
diff --git a/htmlpage/templates/setting-user-add.html b/htmlpage/sync/template/setting-user-add.html
similarity index 100%
rename from htmlpage/templates/setting-user-add.html
rename to htmlpage/sync/template/setting-user-add.html
diff --git a/htmlpage/templates/setting-user.html b/htmlpage/sync/template/setting-user.html
similarity index 100%
rename from htmlpage/templates/setting-user.html
rename to htmlpage/sync/template/setting-user.html
diff --git a/htmlpage/templates/settings.html b/htmlpage/sync/template/settings.html
similarity index 100%
rename from htmlpage/templates/settings.html
rename to htmlpage/sync/template/settings.html
diff --git a/htmlpage/templates/signin.html b/htmlpage/sync/template/signin.html
similarity index 100%
rename from htmlpage/templates/signin.html
rename to htmlpage/sync/template/signin.html
diff --git a/htmlpage/templates/signup.html b/htmlpage/sync/template/signup.html
similarity index 100%
rename from htmlpage/templates/signup.html
rename to htmlpage/sync/template/signup.html
diff --git a/htmlpage/templates/source.html b/htmlpage/sync/template/source.html
similarity index 100%
rename from htmlpage/templates/source.html
rename to htmlpage/sync/template/source.html
diff --git a/htmlpage/time.go b/htmlpage/sync/time.go
similarity index 99%
rename from htmlpage/time.go
rename to htmlpage/sync/time.go
index 24427292..848670ca 100644
--- a/htmlpage/time.go
+++ b/htmlpage/sync/time.go
@@ -1,4 +1,4 @@
-package htmlpage
+package sync
import (
"sort"
diff --git a/htmlpage/sync/types.go b/htmlpage/sync/types.go
new file mode 100644
index 00000000..de475d2c
--- /dev/null
+++ b/htmlpage/sync/types.go
@@ -0,0 +1,121 @@
+package sync
+
+import (
+ "html/template"
+ "time"
+
+ "github.com/Gleipnir-Technology/nidus-sync/notification"
+ "github.com/google/uuid"
+ "github.com/uber/h3-go/v4"
+)
+
+type BreedingSourceSummary struct {
+ ID uuid.UUID
+ Type string
+ LastInspected *time.Time
+ LastTreated *time.Time
+}
+
+type MapMarker struct {
+ LatLng h3.LatLng
+}
+type ComponentMap struct {
+ Center h3.LatLng
+ GeoJSON interface{}
+ MapboxToken string
+ Markers []MapMarker
+ Zoom int
+}
+type ContentAuthenticatedPlaceholder struct {
+ User User
+}
+type ContentCell struct {
+ BreedingSources []BreedingSourceSummary
+ CellBoundary h3.CellBoundary
+ Inspections []Inspection
+ MapData ComponentMap
+ Treatments []Treatment
+ User User
+}
+type ContentMockURLs struct {
+ Dispatch string
+ DispatchResults string
+ ReportConfirmation string
+ ReportDetail string
+ ReportContribute string
+ ReportEvidence string
+ ReportSchedule string
+ ReportUpdate string
+ Root string
+ Setting string
+ SettingIntegration string
+ SettingPesticide string
+ SettingPesticideAdd string
+ SettingUser string
+ SettingUserAdd string
+}
+type ContentMock struct {
+ DistrictName string
+ URLs ContentMockURLs
+}
+type ContentReportDetail struct {
+ NextURL string
+ UpdateURL string
+}
+type ContentReportDiagnostic struct {
+}
+type ContentDashboard struct {
+ CountInspections int
+ CountMosquitoSources int
+ CountServiceRequests int
+ Geo template.JS
+ IsSyncOngoing bool
+ LastSync *time.Time
+ MapData ComponentMap
+ Org string
+ RecentRequests []ServiceRequestSummary
+ User User
+}
+
+type ContentDashboardLoading struct {
+ User User
+}
+
+type ContentPlaceholder struct {
+}
+type ContentSignin struct {
+ InvalidCredentials bool
+}
+type ContentSignup struct{}
+type ContentSource struct {
+ Inspections []Inspection
+ MapData ComponentMap
+ Source *BreedingSourceDetail
+ Traps []TrapNearby
+ Treatments []Treatment
+ //TreatmentCadence TreatmentCadence
+ TreatmentModels []TreatmentModel
+ User User
+}
+type Inspection struct {
+ Action string
+ Date *time.Time
+ Notes string
+ Location string
+ LocationID uuid.UUID
+}
+type Link struct {
+ Href string
+ Title string
+}
+type ServiceRequestSummary struct {
+ Date time.Time
+ Location string
+ Status string
+}
+type User struct {
+ DisplayName string
+ Initials string
+ Notifications []notification.Notification
+ Username string
+}
diff --git a/htmlpage/sync/utils.go b/htmlpage/sync/utils.go
new file mode 100644
index 00000000..827bfe44
--- /dev/null
+++ b/htmlpage/sync/utils.go
@@ -0,0 +1,253 @@
+package sync
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/Gleipnir-Technology/nidus-sync/db"
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+ "github.com/Gleipnir-Technology/nidus-sync/db/sql"
+ "github.com/Gleipnir-Technology/nidus-sync/notification"
+ "github.com/google/uuid"
+ "github.com/stephenafamo/bob"
+ "github.com/stephenafamo/bob/dialect/psql"
+ "github.com/stephenafamo/bob/dialect/psql/sm"
+ "github.com/uber/h3-go/v4"
+)
+
+func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSourceSummary, error) {
+ var results []BreedingSourceSummary
+
+ boundary, err := c.Boundary()
+ if err != nil {
+ return results, fmt.Errorf("Failed to get cell boundary: %w", err)
+ }
+ geom_query := gisStatement(boundary)
+ rows, err := org.Pointlocations(
+ sm.Where(
+ psql.F("ST_Within", "geospatial", geom_query),
+ ),
+ sm.OrderBy("lasttreatdate"),
+ ).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return results, fmt.Errorf("Failed to query rows: %w", err)
+ }
+ for _, r := range rows {
+ var last_inspected *time.Time
+ if !r.Lastinspectdate.IsNull() {
+ l := r.Lastinspectdate.MustGet()
+ last_inspected = &l
+ }
+ var last_treat_date *time.Time
+ if !r.Lasttreatdate.IsNull() {
+ l := r.Lasttreatdate.MustGet()
+ last_treat_date = &l
+ }
+ results = append(results, BreedingSourceSummary{
+ ID: r.Globalid,
+ LastInspected: last_inspected,
+ LastTreated: last_treat_date,
+ Type: r.Habitat.GetOr("none"),
+ })
+ }
+ return results, nil
+}
+func gisStatement(cb h3.CellBoundary) string {
+ var content strings.Builder
+ for i, p := range cb {
+ if i != 0 {
+ content.WriteString(", ")
+ }
+ content.WriteString(fmt.Sprintf("%f %f", p.Lng, p.Lat))
+ }
+ // Repeat the first coordinate to close the polygon
+ content.WriteString(fmt.Sprintf(", %f %f", cb[0].Lng, cb[0].Lat))
+ return fmt.Sprintf("ST_GeomFromText('POLYGON((%s))', 3857)", content.String())
+}
+
+func sourceByGlobalId(ctx context.Context, org *models.Organization, id uuid.UUID) (*BreedingSourceDetail, error) {
+ row, err := org.Pointlocations(
+ sm.Where(models.FieldseekerPointlocations.Columns.Globalid.EQ(psql.Arg(id))),
+ ).One(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get point location: %w", err)
+ }
+ return toTemplateBreedingSource(row), nil
+}
+
+func extractInitials(name string) string {
+ parts := strings.Fields(name)
+ var initials strings.Builder
+
+ for _, part := range parts {
+ if len(part) > 0 {
+ initials.WriteString(strings.ToUpper(string(part[0])))
+ }
+ }
+
+ return initials.String()
+}
+
+func contentForUser(ctx context.Context, user *models.User) (User, error) {
+ notifications, err := notification.ForUser(ctx, user)
+ if err != nil {
+ return User{}, err
+ }
+ return User{
+ DisplayName: user.DisplayName,
+ Initials: extractInitials(user.DisplayName),
+ Notifications: notifications,
+ Username: user.Username,
+ }, nil
+
+}
+
+func trapsBySource(ctx context.Context, org *models.Organization, sourceID uuid.UUID) ([]TrapNearby, error) {
+ locations, err := sql.TrapLocationBySourceID(org.ID, sourceID).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to query rows: %w", err)
+ }
+
+ location_ids := make([]uuid.UUID, 0)
+ var args []bob.Expression
+ for _, location := range locations {
+ location_ids = append(location_ids, location.TrapLocationGlobalid)
+ args = append(args, psql.Arg(location.TrapLocationGlobalid))
+ }
+ /*
+ trap_data, err := org.FSTrapdata(
+ sm.Where(
+ models.FSTrapdata.Columns.LocID.In(args...),
+ ),
+ sm.OrderBy("enddatetime"),
+ ).All(ctx, db.PGInstance.BobDB)
+ */
+
+ /*
+ query := org.FSTrapdata(
+ sm.From(
+ psql.Select(
+ sm.From(psql.F("ROW_NUMBER")(
+ fm.Over(
+ wm.PartitionBy(models.FSTrapdata.Columns.LocID),
+ wm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
+ ),
+ )).As("row_num"),
+ sm.Where(models.FSTrapdata.Columns.LocID.In(args...))),
+ ),
+ sm.Where(psql.Quote("row_num").LTE(psql.Arg(10))),
+ sm.OrderBy(models.FSTrapdata.Columns.LocID),
+ sm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
+ )
+ */
+ /*
+ query := psql.Select(
+ sm.From(
+ psql.Select(
+ sm.Columns(
+ models.FSTrapdata.Columns.Globalid,
+ psql.F("ROW_NUMBER")(
+ fm.Over(
+ wm.PartitionBy(models.FSTrapdata.Columns.LocID),
+ wm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
+ ),
+ ).As("row_num"),
+ sm.From(models.FSTrapdata.Name()),
+ ),
+ sm.Where(models.FSTrapdata.Columns.LocID.In(args...))),
+ ),
+ sm.Where(psql.Quote("row_num").LTE(psql.Arg(10))),
+ sm.OrderBy(models.FSTrapdata.Columns.LocID),
+ sm.OrderBy(models.FSTrapdata.Columns.Enddatetime).Desc(),
+ )
+ log.Info().Str("trapdata", queryToString(query)).Msg("Getting trap data")
+ trap_data, err := query.Exec(ctx, db.PGInstance.BobDB)
+ */
+
+ trap_data, err := sql.TrapDataByLocationIDRecent(org.ID, location_ids).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to query trap data: %w", err)
+ }
+
+ counts, err := sql.TrapCountByLocationID(org.ID, location_ids).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to query trap counts: %w", err)
+ }
+
+ traps, err := toTemplateTraps(locations, trap_data, counts)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to convert trap data: %w", err)
+ }
+ return traps, nil
+}
+
+func treatmentsBySource(ctx context.Context, org *models.Organization, sourceID uuid.UUID) ([]Treatment, error) {
+ var results []Treatment
+ rows, err := org.Treatments(
+ sm.Where(
+ models.FieldseekerTreatments.Columns.Pointlocid.EQ(psql.Arg(sourceID)),
+ ),
+ sm.OrderBy("enddatetime").Desc(),
+ ).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return results, fmt.Errorf("Failed to query rows: %w", err)
+ }
+ //log.Info().Int("row count", len(rows)).Msg("Getting treatments")
+ return toTemplateTreatment(rows)
+}
+
+func treatmentsByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]Treatment, error) {
+ var results []Treatment
+ boundary, err := c.Boundary()
+ if err != nil {
+ return results, fmt.Errorf("Failed to get cell boundary: %w", err)
+ }
+ geom_query := gisStatement(boundary)
+ rows, err := org.Treatments(
+ sm.Where(
+ psql.F("ST_Within", "geospatial", geom_query),
+ ),
+ sm.OrderBy("pointlocid"),
+ sm.OrderBy("enddatetime"),
+ ).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return results, fmt.Errorf("Failed to query rows: %w", err)
+ }
+ return toTemplateTreatment(rows)
+}
+func inspectionsByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]Inspection, error) {
+ var results []Inspection
+
+ boundary, err := c.Boundary()
+ if err != nil {
+ return results, fmt.Errorf("Failed to get cell boundary: %w", err)
+ }
+ geom_query := gisStatement(boundary)
+ rows, err := org.Mosquitoinspections(
+ sm.Where(
+ psql.F("ST_Within", "geospatial", geom_query),
+ ),
+ sm.OrderBy("pointlocid"),
+ sm.OrderBy("enddatetime"),
+ ).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return results, fmt.Errorf("Failed to query rows: %w", err)
+ }
+ return toTemplateInspection(rows)
+}
+func inspectionsBySource(ctx context.Context, org *models.Organization, sourceID uuid.UUID) ([]Inspection, error) {
+ var results []Inspection
+
+ rows, err := org.Mosquitoinspections(
+ sm.Where(
+ models.FieldseekerMosquitoinspections.Columns.Pointlocid.EQ(psql.Arg(sourceID)),
+ ),
+ sm.OrderBy("enddatetime").Desc(),
+ ).All(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return results, fmt.Errorf("Failed to query rows: %w", err)
+ }
+ return toTemplateInspection(rows)
+}
diff --git a/report/endpoint.go b/report/endpoint.go
index 8f97bca2..74f2a86a 100644
--- a/report/endpoint.go
+++ b/report/endpoint.go
@@ -1,9 +1,10 @@
package report
import (
- "fmt"
"net/http"
+ "github.com/Gleipnir-Technology/nidus-sync/htmlpage"
+ "github.com/Gleipnir-Technology/nidus-sync/htmlpage/public-reports"
"github.com/go-chi/chi/v5"
)
@@ -14,5 +15,9 @@ func Router() chi.Router {
}
func getRoot(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Herro.")
+ htmlpage.RenderOrError(
+ w,
+ publicreports.Root,
+ publicreports.RootContext{},
+ )
}