From e2549f03171d6daf01e226dd29cff12a1d8b1f9e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 15 Jan 2026 22:12:35 +0000 Subject: [PATCH] Make trap page that shows collection information --- sync/dash.go | 72 ++++++++++++++++++++- sync/model_conversion.go | 74 +++++++++++++++++++--- sync/routes.go | 1 + sync/template/trap.html | 131 +++++++++++++++++++++++++++++++++++++++ sync/types.go | 10 --- sync/utils.go | 25 +++++++- 6 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 sync/template/trap.html diff --git a/sync/dash.go b/sync/dash.go index 04b464ac..11858361 100644 --- a/sync/dash.go +++ b/sync/dash.go @@ -27,18 +27,34 @@ var ( districtT = buildTemplate("district", "base") settingsT = buildTemplate("settings", "authenticated") sourceT = buildTemplate("source", "authenticated") + trapT = buildTemplate("trap", "authenticated") ) type Config struct { URLTegola string } +type ContentSource struct { + Inspections []Inspection + MapData ComponentMap + Source *BreedingSourceDetail + Traps []TrapNearby + Treatments []Treatment + //TreatmentCadence TreatmentCadence + TreatmentModels []TreatmentModel + User User +} +type ContentTrap struct { + MapData ComponentMap + Trap Trap + User User +} type ContextCell struct { BreedingSources []BreedingSourceSummary CellBoundary h3.CellBoundary Inspections []Inspection MapData ComponentMap - Traps []Trap + Traps []TrapSummary Treatments []Treatment User User } @@ -133,6 +149,20 @@ func getSource(w http.ResponseWriter, r *http.Request, u *models.User) { source(w, r, u, globalid) } +func getTrap(w http.ResponseWriter, r *http.Request, u *models.User) { + globalid_s := chi.URLParam(r, "globalid") + if globalid_s == "" { + respondError(w, "No globalid provided", nil, http.StatusBadRequest) + return + } + globalid, err := uuid.Parse(globalid_s) + if err != nil { + respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest) + return + } + trap(w, r, u, globalid) +} + 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 { @@ -342,3 +372,43 @@ func source(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.U htmlpage.RenderOrError(w, sourceT, data) } + +func trap(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 + } + t, err := trapByGlobalId(r.Context(), org, id) + if err != nil { + respondError(w, "Failed to get trap", err, http.StatusInternalServerError) + return + } + latlng, err := t.H3Cell.LatLng() + if err != nil { + respondError(w, "Failed to get latlng", err, http.StatusInternalServerError) + return + } + data := ContentTrap{ + MapData: ComponentMap{ + Center: latlng, + //GeoJSON: + MapboxToken: config.MapboxToken, + Markers: []MapMarker{ + MapMarker{ + LatLng: latlng, + }, + }, + Zoom: 13, + }, + Trap: t, + User: userContent, + } + + htmlpage.RenderOrError(w, trapT, data) +} diff --git a/sync/model_conversion.go b/sync/model_conversion.go index 31156274..9b4fa373 100644 --- a/sync/model_conversion.go +++ b/sync/model_conversion.go @@ -76,10 +76,13 @@ type BreedingSourceDetail struct { Comments string `json:"comments"` } -type TrapNearby struct { - Counts []*TrapCount - Distance string - ID uuid.UUID +type Trap struct { + Active bool + Comments string + Collections []TrapData + Description string + GlobalID uuid.UUID + H3Cell h3.Cell } type TrapCount struct { @@ -152,9 +155,18 @@ type TrapData struct { LastEditedDate *time.Time `json:"lastEditedDate"` LastEditedUser string `json:"lastEditedUser"` Comments string `json:"comments"` + + // Stuff I actually use + Count TrapCount } -type Trap struct { +type TrapNearby struct { + Counts []*TrapCount + Distance string + ID uuid.UUID +} + +type TrapSummary struct { Active bool Comments string Description string @@ -169,9 +181,57 @@ type Treatment struct { Product string } -func toTemplateTrap(traps models.FieldseekerTraplocationSlice) (results []Trap, err error) { +func toTemplateTrap(trap *models.FieldseekerTraplocation, trap_data []sql.TrapDataByLocationIDRecentRow, count_slice []sql.TrapCountByLocationIDRow) (result Trap, err error) { + log.Debug().Str("globalid", trap.Globalid.String()).Msg("Working on trap") + cell, err := h3utils.ToCell(trap.H3cell.MustGet()) + if err != nil { + return result, fmt.Errorf("Failed to convert h3 cell: %w", err) + } + + count_by_trapdata_id := make(map[uuid.UUID]TrapCount, 0) + for _, count := range count_slice { + count_by_trapdata_id[count.TrapdataGlobalid] = TrapCount{ + Ended: count.TrapdataEnddate.MustGet(), + Females: int(count.TotalFemales), + Males: int(count.TotalMales), + Total: int(count.Total), + } + } + + data_by_id := make(map[uuid.UUID]TrapData, 0) + for _, dt := range trap_data { + if dt.LocID != trap.Globalid { + return result, fmt.Errorf("Bad query") + } + log.Debug().Str("trapdata", dt.Globalid.String()).Msg("Aggregating trapdata") + count, ok := count_by_trapdata_id[dt.Globalid] + if !ok { + count = TrapCount{} + } + data_by_id[dt.Globalid] = TrapData{ + Count: count, + EndDateTime: &dt.Enddatetime, + GlobalID: dt.Globalid, + } + } + data := make([]TrapData, 0) + for _, v := range data_by_id { + data = append(data, v) + } + + return Trap{ + Active: toBool16Or(trap.Active, false), + Comments: trap.Comments.GetOr(""), + Collections: data, + Description: trap.Description.GetOr(""), + GlobalID: trap.Globalid, + H3Cell: cell, + }, nil +} + +func toTemplateTrapSummary(traps models.FieldseekerTraplocationSlice) (results []TrapSummary, err error) { for _, t := range traps { - results = append(results, Trap{ + results = append(results, TrapSummary{ Active: toBool16Or(t.Active, false), Comments: t.Comments.GetOr(""), Description: t.Description.GetOr(""), diff --git a/sync/routes.go b/sync/routes.go index 89cf6444..6fa8ae8d 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -66,6 +66,7 @@ func Router() chi.Router { r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings)) r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) + r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap)) htmlpage.AddStaticRoute(r, "/static") return r diff --git a/sync/template/trap.html b/sync/template/trap.html new file mode 100644 index 00000000..3f22a94e --- /dev/null +++ b/sync/template/trap.html @@ -0,0 +1,131 @@ +{{template "authenticated.html" .}} + +{{define "title"}}Dash{{end}} +{{define "extraheader"}} +{{template "map" .MapData}} + +{{end}} +{{define "content"}} +
+ +
+
+

Trap Detail

+
+
+ +
+
+
+
+
+
Trap ID: {{ .Trap.GlobalID }}
+ + + + + + + + + + + + + +
Active:{{ .Trap.Active }}
Comments:{{ .Trap.Comments }}
Description:{{ .Trap.Description }}
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+

Trap Collections

+
+ + + + + + + + + + + + + {{ range .Trap.Collections }} + + + + + + + + {{ end }} + +
Collection DateCollection IDFemalesMaleTotal
{{ .EndDateTime|timeSince }}{{ .GlobalID }}{{ .Count.Females }}{{ .Count.Males }}{{ .Count.Total }}
+
+
+
+
+{{end}} diff --git a/sync/types.go b/sync/types.go index a31b3712..8306d280 100644 --- a/sync/types.go +++ b/sync/types.go @@ -65,16 +65,6 @@ 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 diff --git a/sync/utils.go b/sync/utils.go index 7cba5560..94f2dcd7 100644 --- a/sync/utils.go +++ b/sync/utils.go @@ -203,7 +203,28 @@ func treatmentsBySource(ctx context.Context, org *models.Organization, sourceID return toTemplateTreatment(rows) } -func trapsByCell(ctx context.Context, org *models.Organization, c h3.Cell) (results []Trap, err error) { +func trapByGlobalId(ctx context.Context, org *models.Organization, id uuid.UUID) (result Trap, err error) { + row, err := org.Traplocations( + sm.Where(models.FieldseekerTraplocations.Columns.Globalid.EQ(psql.Arg(id))), + ).One(ctx, db.PGInstance.BobDB) + if err != nil { + return result, fmt.Errorf("Failed to get trap location: %w", err) + } + + trap_data, err := sql.TrapDataByLocationIDRecent(org.ID, []uuid.UUID{id}).All(ctx, db.PGInstance.BobDB) + if err != nil { + return result, fmt.Errorf("Failed to query trap data: %w", err) + } + + counts, err := sql.TrapCountByLocationID(org.ID, []uuid.UUID{id}).All(ctx, db.PGInstance.BobDB) + if err != nil { + return result, fmt.Errorf("Failed to query trap counts: %w", err) + } + + return toTemplateTrap(row, trap_data, counts) +} + +func trapsByCell(ctx context.Context, org *models.Organization, c h3.Cell) (results []TrapSummary, err error) { boundary, err := c.Boundary() if err != nil { return results, fmt.Errorf("Failed to get cell boundary: %w", err) @@ -218,7 +239,7 @@ func trapsByCell(ctx context.Context, org *models.Organization, c h3.Cell) (resu if err != nil { return results, fmt.Errorf("Failed to query rows: %w", err) } - return toTemplateTrap(rows) + return toTemplateTrapSummary(rows) } func treatmentsByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]Treatment, error) {