Split up html page logic into site-specific packages
This is a significant overhaul to make it possible to serve totally different templates with different components for the different sites.
This commit is contained in:
parent
572b8a9de9
commit
bacd452346
53 changed files with 1120 additions and 754 deletions
22
endpoint.go
22
endpoint.go
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/background"
|
"github.com/Gleipnir-Technology/nidus-sync/background"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
"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/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
"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)
|
respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
htmlpage.Cell(r.Context(), w, user, cell)
|
sync.Cell(r.Context(), w, user, cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFavicon(w http.ResponseWriter, r *http.Request) {
|
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)
|
http.Redirect(w, r, "/?next=/oauth/refresh", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
htmlpage.OauthPrompt(w, user)
|
sync.OauthPrompt(w, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQRCodeReport(w http.ResponseWriter, r *http.Request) {
|
func getQRCodeReport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -148,7 +148,7 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
if user == nil {
|
if user == nil {
|
||||||
errorCode := r.URL.Query().Get("error")
|
errorCode := r.URL.Query().Get("error")
|
||||||
htmlpage.Signin(w, errorCode)
|
sync.Signin(w, errorCode)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
has, err := background.HasFieldseekerConnection(r.Context(), user)
|
has, err := background.HasFieldseekerConnection(r.Context(), user)
|
||||||
|
|
@ -157,10 +157,10 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
htmlpage.Dashboard(r.Context(), w, user)
|
sync.Dashboard(r.Context(), w, user)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
htmlpage.OauthPrompt(w, user)
|
sync.OauthPrompt(w, user)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,16 +170,16 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) {
|
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) {
|
func getSignin(w http.ResponseWriter, r *http.Request) {
|
||||||
errorCode := r.URL.Query().Get("error")
|
errorCode := r.URL.Query().Get("error")
|
||||||
htmlpage.Signin(w, errorCode)
|
sync.Signin(w, errorCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSignup(w http.ResponseWriter, r *http.Request) {
|
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) {
|
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)
|
respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
htmlpage.Source(w, r, u, globalid)
|
sync.Source(w, r, u, globalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func postSMS(w http.ResponseWriter, r *http.Request) {
|
func postSMS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -327,6 +327,6 @@ func renderMock(templateName string) http.HandlerFunc {
|
||||||
if code == "" {
|
if code == "" {
|
||||||
code = "abc-123"
|
code = "abc-123"
|
||||||
}
|
}
|
||||||
htmlpage.Mock(templateName, w, code)
|
sync.Mock(templateName, w, code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
htmlpage/h3.go
Normal file
39
htmlpage/h3.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
798
htmlpage/html.go
798
htmlpage/html.go
|
|
@ -2,7 +2,6 @@ package htmlpage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -11,194 +10,30 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/background"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
"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/aarondl/opt/null"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
"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 TemplatesByFilename = make(map[string]BuiltTemplate, 0)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type BuiltTemplate struct {
|
type BuiltTemplate struct {
|
||||||
files []string
|
files []string
|
||||||
name 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
|
template *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapMarker struct {
|
func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error {
|
||||||
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"
|
|
||||||
if bt.template == nil {
|
if bt.template == nil {
|
||||||
|
name := path.Base(bt.files[0])
|
||||||
templ, err := parseFromDisk(bt.files)
|
templ, err := parseFromDisk(bt.files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse template file: %w", err)
|
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: "))
|
w.Write([]byte("Failed to read from disk: "))
|
||||||
return errors.New("Template parsing failed")
|
return errors.New("Template parsing failed")
|
||||||
}
|
}
|
||||||
|
log.Debug().Str("name", templ.Name()).Msg("Parsed template")
|
||||||
return templ.ExecuteTemplate(w, name, data)
|
return templ.ExecuteTemplate(w, name, data)
|
||||||
} else {
|
} else {
|
||||||
|
name := path.Base(bt.files[0])
|
||||||
return bt.template.ExecuteTemplate(w, name, data)
|
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 {
|
func bigNumber(n int) string {
|
||||||
// Convert the number to a string
|
// Convert the number to a string
|
||||||
numStr := strconv.FormatInt(int64(n), 10)
|
numStr := strconv.FormatInt(int64(n), 10)
|
||||||
|
|
@ -229,318 +107,6 @@ func bigNumber(n int) string {
|
||||||
return result.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 {
|
func makeFuncMap() template.FuncMap {
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"bigNumber": bigNumber,
|
"bigNumber": bigNumber,
|
||||||
|
|
@ -555,63 +121,26 @@ func makeFuncMap() template.FuncMap {
|
||||||
}
|
}
|
||||||
return funcMap
|
return funcMap
|
||||||
}
|
}
|
||||||
func newBuiltTemplate(name string, files ...string) *BuiltTemplate {
|
func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template {
|
||||||
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 {
|
|
||||||
funcMap := makeFuncMap()
|
funcMap := makeFuncMap()
|
||||||
// Remap the file names to embedded paths
|
// Remap the file names to embedded paths
|
||||||
paths := make([]string, 0)
|
embeddedFilePaths := make([]string, 0)
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
paths = append(paths, "templates/"+f+".html")
|
embeddedFilePaths = append(embeddedFilePaths, strings.TrimPrefix(f, subdir))
|
||||||
}
|
}
|
||||||
for _, f := range components {
|
name := path.Base(embeddedFilePaths[0])
|
||||||
paths = append(paths, "templates/components/"+f+".html")
|
log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
|
||||||
}
|
|
||||||
name := files[0]
|
|
||||||
return template.Must(
|
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) {
|
func parseFromDisk(files []string) (*template.Template, error) {
|
||||||
funcMap := makeFuncMap()
|
funcMap := makeFuncMap()
|
||||||
paths := make([]string, 0)
|
name := path.Base(files[0])
|
||||||
for _, f := range files {
|
log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
|
||||||
paths = append(paths, "templates/"+f+".html")
|
templ, err := template.New(name).Funcs(funcMap).ParseFiles(files...)
|
||||||
}
|
|
||||||
name := files[0] + ".html"
|
|
||||||
for _, f := range components {
|
|
||||||
paths = append(paths, "templates/components/"+f+".html")
|
|
||||||
}
|
|
||||||
templ, err := template.New(name).Funcs(funcMap).ParseFiles(paths...)
|
|
||||||
if err != nil {
|
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
|
return templ, nil
|
||||||
}
|
}
|
||||||
|
|
@ -750,203 +279,6 @@ func timeSince(t *time.Time) string {
|
||||||
return fmt.Sprintf("%d days ago", int(days))
|
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 {
|
func uuidShort(uuid uuid.UUID) string {
|
||||||
s := uuid.String()
|
s := uuid.String()
|
||||||
if len(s) < 7 {
|
if len(s) < 7 {
|
||||||
|
|
@ -955,13 +287,3 @@ func uuidShort(uuid uuid.UUID) string {
|
||||||
|
|
||||||
return s[:3] + "..." + s[len(s)-4:]
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
31
htmlpage/public-reports/page.go
Normal file
31
htmlpage/public-reports/page.go
Normal file
|
|
@ -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...)
|
||||||
|
}
|
||||||
224
htmlpage/public-reports/template/root.html
Normal file
224
htmlpage/public-reports/template/root.html
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
{{template "base.html" .}}
|
||||||
|
|
||||||
|
{{define "title"}}Login{{end}}
|
||||||
|
{{define "extraheader"}}
|
||||||
|
<style>
|
||||||
|
.entry-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.entry-box {
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.entry-header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.logo-area {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.logo-placeholder {
|
||||||
|
width: 120px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.method-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.method-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.method-title i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.sms-mockup {
|
||||||
|
max-width: 300px;
|
||||||
|
background-color: #dcf8c6;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
.qr-code-placeholder {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #ced4da;
|
||||||
|
}
|
||||||
|
.email-mockup {
|
||||||
|
max-width: 550px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
.email-header {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.door-hanger {
|
||||||
|
max-width: 300px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 2px solid #dc3545;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.door-hanger:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 25px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 2px solid #dc3545;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="entry-container">
|
||||||
|
<div class="entry-box">
|
||||||
|
<div class="entry-header">
|
||||||
|
<h1>Green Pool Reporting</h1>
|
||||||
|
<p class="text-muted">Entry Points Diagnostic Page</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
This page demonstrates the various ways customers can access the Green Pool Reporting system.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SMS Entry Point -->
|
||||||
|
<div class="method-section">
|
||||||
|
<div class="method-title">
|
||||||
|
<i class="bi bi-chat-dots-fill text-primary"></i>
|
||||||
|
<h3>Text Message Entry Point</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Customers will receive the following text message with a link to begin the reporting process:</p>
|
||||||
|
|
||||||
|
<div class="sms-mockup">
|
||||||
|
<strong>Vector Control:</strong> We noticed a potential green pool at your property. Please tap the link to report status or schedule inspection: <a href="/report-detail">/report-detail</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p><strong>SMS Details:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Sent via automated system after aerial detection</li>
|
||||||
|
<li>Contains unique tracking link for each property</li>
|
||||||
|
<li>Customers tap link to open mobile browser</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Door Hanger Entry Point -->
|
||||||
|
<div class="method-section">
|
||||||
|
<div class="method-title">
|
||||||
|
<i class="bi bi-qr-code text-success"></i>
|
||||||
|
<h3>Door Hanger QR Code Entry Point</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Inspectors will leave door hangers with a QR code for properties where no one is home:</p>
|
||||||
|
|
||||||
|
<div class="door-hanger">
|
||||||
|
<h5>IMPORTANT NOTICE</h5>
|
||||||
|
<p>We visited regarding a potential mosquito breeding site.</p>
|
||||||
|
|
||||||
|
<img src="/qr-code/report/t78fd3" width="256" height="256"/>
|
||||||
|
|
||||||
|
<p><strong>Scan this code</strong> with your phone camera to report your pool status or schedule an inspection.</p>
|
||||||
|
<p class="small text-muted">Or visit: /report-detail</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p><strong>Door Hanger Details:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Physical notices left on the door handle</li>
|
||||||
|
<li>QR code contains property-specific link</li>
|
||||||
|
<li>Fallback URL provided for manual entry</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Entry Point -->
|
||||||
|
<div class="method-section">
|
||||||
|
<div class="method-title">
|
||||||
|
<i class="bi bi-envelope-fill text-danger"></i>
|
||||||
|
<h3>Email Notification Entry Point</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Property owners will receive this email as a follow-up to other communication attempts:</p>
|
||||||
|
|
||||||
|
<div class="email-mockup">
|
||||||
|
<div class="email-header">
|
||||||
|
<strong>From:</strong> Green Pool Response Team <noreply@vectorcontrol.county.gov><br>
|
||||||
|
<strong>To:</strong> Property Owner <resident@example.com><br>
|
||||||
|
<strong>Subject:</strong> Action Required: Green Pool Detected at Your Property
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Dear Property Owner,</p>
|
||||||
|
|
||||||
|
<p>Our recent surveillance has identified a potential unmaintained swimming pool at your property located at <strong>123 Main Street</strong>. Untreated pools can become mosquito breeding grounds and pose public health risks, including the spread of West Nile virus and other diseases.</p>
|
||||||
|
|
||||||
|
<div class="text-center my-4">
|
||||||
|
<a href="/report/t78fd3" class="btn btn-primary">Report Pool Status or Schedule Inspection</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Please click the button above or visit <a href="/report-detail">/report-detail</a> 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.</p>
|
||||||
|
|
||||||
|
<p>Thank you for helping keep our community safe and healthy.</p>
|
||||||
|
|
||||||
|
<p>Sincerely,<br>
|
||||||
|
Vector Control Department<br>
|
||||||
|
County Health Services</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p><strong>Email Details:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Sent as follow-up or for property owners with registered email addresses</li>
|
||||||
|
<li>Contains clear call-to-action button and alternative text link</li>
|
||||||
|
<li>Explains reason for contact and next steps</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<a href="/" class="btn btn-outline-primary">Back to Dashboard</a>
|
||||||
|
<a href="/report-detail" class="btn btn-success">Test Reporting Flow</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package htmlpage
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
352
htmlpage/sync/page.go
Normal file
352
htmlpage/sync/page.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
19
htmlpage/sync/template/base.html
Normal file
19
htmlpage/sync/template/base.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{template "title" .}} - Nidus Sync</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="/static/vendor/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||||
|
<!-- Fontawesome Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
{{block "extraheader" .}} {{end}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "content" .}}
|
||||||
|
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package htmlpage
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
121
htmlpage/sync/types.go
Normal file
121
htmlpage/sync/types.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
253
htmlpage/sync/utils.go
Normal file
253
htmlpage/sync/utils.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package report
|
package report
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/htmlpage"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/htmlpage/public-reports"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14,5 +15,9 @@ func Router() chi.Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRoot(w http.ResponseWriter, r *http.Request) {
|
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, "Herro.")
|
htmlpage.RenderOrError(
|
||||||
|
w,
|
||||||
|
publicreports.Root,
|
||||||
|
publicreports.RootContext{},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue