package htmlpage import ( "bytes" "context" "embed" "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/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 } type BuiltTemplate struct { files []string name string 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" if bt.template == nil { templ, err := parseFromDisk(bt.files) if err != nil { return fmt.Errorf("Failed to parse template file: %w", err) } if templ == nil { w.Write([]byte("Failed to read from disk: ")) return errors.New("Template parsing failed") } return templ.ExecuteTemplate(w, name, data) } else { return bt.template.ExecuteTemplate(w, name, data) } } func bigNumber(n int) string { // Convert the number to a string numStr := strconv.FormatInt(int64(n), 10) // Add commas every three digits from the right var result strings.Builder for i, char := range numStr { if i > 0 && (len(numStr)-i)%3 == 0 { result.WriteByte(',') } result.WriteRune(char) } 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, "GISStatement": gisStatement, "latLngDisplay": latLngDisplay, "timeAsRelativeDate": timeAsRelativeDate, "timeDelta": timeDelta, "timeElapsed": timeElapsed, "timeInterval": timeInterval, "timeSince": timeSince, "uuidShort": uuidShort, } 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 { funcMap := makeFuncMap() // Remap the file names to embedded paths paths := make([]string, 0) for _, f := range files { paths = append(paths, "templates/"+f+".html") } for _, f := range components { paths = append(paths, "templates/components/"+f+".html") } name := files[0] return template.Must( template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, paths...)) } 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...) if err != nil { return nil, fmt.Errorf("Failed to parse %s: %w", paths, err) } return templ, nil } func timeAsRelativeDate(d time.Time) string { return d.Format("01-02") } // FormatTimeDuration returns a human-readable string representing a time.Duration // as "X units early" or "X units late" func timeDelta(d time.Duration) string { suffix := "late" if d < 0 { suffix = "early" d = -d // Make duration positive for calculations } const ( day = 24 * time.Hour week = 7 * day ) log.Info().Int64("delta", int64(d)).Str("suffix", suffix).Msg("Time delta") switch { case d >= week: weeks := d / week if weeks == 1 { return "1 week " + suffix } return fmt.Sprintf("%d weeks %s", weeks, suffix) case d >= day: days := d / day if days == 1 { return "1 day " + suffix } return fmt.Sprintf("%d days %s", days, suffix) case d >= time.Hour: hours := d / time.Hour if hours == 1 { return "1 hour " + suffix } return fmt.Sprintf("%d hours %s", hours, suffix) case d >= time.Minute: minutes := d / time.Minute if minutes == 1 { return "1 minute " + suffix } return fmt.Sprintf("%d minutes %s", minutes, suffix) default: seconds := d / time.Second if seconds == 1 { return "1 second " + suffix } return fmt.Sprintf("%d seconds %s", seconds, suffix) } } func timeElapsed(seconds null.Val[float32]) string { if !seconds.IsValue() { return "none" } s := int(seconds.MustGet()) hours := s / 3600 remainder := s - (hours * 3600) minutes := remainder / 60 remainder = remainder - (minutes * 60) if hours > 0 { return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainder) } else if minutes > 0 { return fmt.Sprintf("%02d:%02d", minutes, remainder) } else { return fmt.Sprintf("%d seconds", remainder) } } func timeInterval(d time.Duration) string { seconds := d.Seconds() // Less than 120 seconds -> show in seconds if seconds < 120 { return fmt.Sprintf("every %d seconds", int(math.Round(seconds))) } minutes := d.Minutes() // Less than 120 minutes -> show in minutes if minutes < 120 { return fmt.Sprintf("every %d minutes", int(math.Round(minutes))) } hours := d.Hours() // Less than 48 hours -> show in hours if hours < 48 { return fmt.Sprintf("every %d hours", int(math.Round(hours))) } days := hours / 24 // Less than 14 days -> show in days if days < 14 { return fmt.Sprintf("every %d days", int(math.Round(days))) } weeks := days / 7 // Less than 8 weeks -> show in weeks if weeks < 8 { return fmt.Sprintf("every %d weeks", int(math.Round(weeks))) } months := days / 30 // Less than 24 months -> show in months if months < 24 { return fmt.Sprintf("every %d months", int(math.Round(months))) } years := days / 365 return fmt.Sprintf("every %d years", int(math.Round(years))) } func timeSince(t *time.Time) string { if t == nil { return "never" } now := time.Now() diff := now.Sub(*t) hours := diff.Hours() if hours < 1 { minutes := diff.Minutes() return fmt.Sprintf("%d minutes ago", int(minutes)) } else if hours < 24 { return fmt.Sprintf("%d hours ago", int(hours)) } else { days := hours / 24 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 { return s // Return as is if too short } 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 }