Make trap page that shows collection information
This commit is contained in:
parent
885b58a0ab
commit
e2549f0317
6 changed files with 293 additions and 20 deletions
72
sync/dash.go
72
sync/dash.go
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(""),
|
||||||
|
|
|
||||||
|
|
@ -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
131
sync/template/trap.html
Normal 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}}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue