Make trap page that shows collection information

This commit is contained in:
Eli Ribble 2026-01-15 22:12:35 +00:00
parent 885b58a0ab
commit e2549f0317
No known key found for this signature in database
6 changed files with 293 additions and 20 deletions

View file

@ -27,18 +27,34 @@ var (
districtT = buildTemplate("district", "base") districtT = buildTemplate("district", "base")
settingsT = buildTemplate("settings", "authenticated") settingsT = buildTemplate("settings", "authenticated")
sourceT = buildTemplate("source", "authenticated") sourceT = buildTemplate("source", "authenticated")
trapT = buildTemplate("trap", "authenticated")
) )
type Config struct { type Config struct {
URLTegola string 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 { type ContextCell struct {
BreedingSources []BreedingSourceSummary BreedingSources []BreedingSourceSummary
CellBoundary h3.CellBoundary CellBoundary h3.CellBoundary
Inspections []Inspection Inspections []Inspection
MapData ComponentMap MapData ComponentMap
Traps []Trap Traps []TrapSummary
Treatments []Treatment Treatments []Treatment
User User User User
} }
@ -133,6 +149,20 @@ func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
source(w, r, u, globalid) 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) { func cell(ctx context.Context, w http.ResponseWriter, user *models.User, c int64) {
org, err := user.Organization().One(ctx, db.PGInstance.BobDB) org, err := user.Organization().One(ctx, db.PGInstance.BobDB)
if err != nil { 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) 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)
}

View file

@ -76,10 +76,13 @@ type BreedingSourceDetail struct {
Comments string `json:"comments"` Comments string `json:"comments"`
} }
type TrapNearby struct { type Trap struct {
Counts []*TrapCount Active bool
Distance string Comments string
ID uuid.UUID Collections []TrapData
Description string
GlobalID uuid.UUID
H3Cell h3.Cell
} }
type TrapCount struct { type TrapCount struct {
@ -152,9 +155,18 @@ type TrapData struct {
LastEditedDate *time.Time `json:"lastEditedDate"` LastEditedDate *time.Time `json:"lastEditedDate"`
LastEditedUser string `json:"lastEditedUser"` LastEditedUser string `json:"lastEditedUser"`
Comments string `json:"comments"` 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 Active bool
Comments string Comments string
Description string Description string
@ -169,9 +181,57 @@ type Treatment struct {
Product string 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 { for _, t := range traps {
results = append(results, Trap{ results = append(results, TrapSummary{
Active: toBool16Or(t.Active, false), Active: toBool16Or(t.Active, false),
Comments: t.Comments.GetOr(""), Comments: t.Comments.GetOr(""),
Description: t.Description.GetOr(""), Description: t.Description.GetOr(""),

View file

@ -66,6 +66,7 @@ func Router() chi.Router {
r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails))
r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings)) r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings))
r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource))
r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap))
htmlpage.AddStaticRoute(r, "/static") htmlpage.AddStaticRoute(r, "/static")
return r return r

131
sync/template/trap.html Normal file
View file

@ -0,0 +1,131 @@
{{template "authenticated.html" .}}
{{define "title"}}Dash{{end}}
{{define "extraheader"}}
{{template "map" .MapData}}
<style>
.info-table {
width: 100%;
}
.info-table td {
padding: 5px 0;
}
.info-label {
font-weight: 600;
vertical-align: top;
}
.map-container {
background-color: #e9ecef;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
margin-bottom: 20px;
}
.section-header {
margin-top: 30px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.source-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.time-delta-positive {
color: #dc3545; /* red for late */
}
.time-delta-negative {
color: #28a745; /* green for early */
}
.time-delta-neutral {
color: #6c757d; /* gray for on time */
}
</style>
{{end}}
{{define "content"}}
<div class="container mt-4 mb-5">
<!-- Trap Header Section -->
<div class="row mb-2">
<div class="col-12">
<h1>Trap Detail</h1>
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<div class="source-info">
<div class="row">
<div class="col-md-6">
<div class="source-id">Trap ID: {{ .Trap.GlobalID }}</div>
<table class="info-table">
<tr>
<td class="info-label">Active:</td>
<td>{{ .Trap.Active }}</td>
</tr>
<tr>
<td class="info-label">Comments:</td>
<td>{{ .Trap.Comments }}</td>
</tr>
<tr>
<td class="info-label">Description:</td>
<td>{{ .Trap.Description }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Map Section -->
<div class="row">
<div class="col-12">
<div class="map-container">
<div id="map"></div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<h2 class="section-header">Trap Collections</h2>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Collection Date</th>
<th>Collection ID</th>
<th>Females</th>
<th>Male</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<!-- Trap 1 with multiple collections -->
{{ range .Trap.Collections }}
<tr>
<td>{{ .EndDateTime|timeSince }}</td>
<td>{{ .GlobalID }}</td>
<td>{{ .Count.Females }}</td>
<td>{{ .Count.Males }}</td>
<td>{{ .Count.Total }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -65,16 +65,6 @@ type ContentSignin struct {
InvalidCredentials bool InvalidCredentials bool
} }
type ContentSignup struct{} 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 { type Inspection struct {
Action string Action string
Date *time.Time Date *time.Time

View file

@ -203,7 +203,28 @@ func treatmentsBySource(ctx context.Context, org *models.Organization, sourceID
return toTemplateTreatment(rows) 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() boundary, err := c.Boundary()
if err != nil { if err != nil {
return results, fmt.Errorf("Failed to get cell boundary: %w", err) 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 { if err != nil {
return results, fmt.Errorf("Failed to query rows: %w", err) 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) { func treatmentsByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]Treatment, error) {