Add breeding source detail page

A bunch of it is currently placeholders, but I got the map marker
working so I'm saving it.
This commit is contained in:
Eli Ribble 2025-11-20 14:56:34 +00:00
parent 6dca03fd0a
commit f599d831c8
No known key found for this signature in database
8 changed files with 740 additions and 6 deletions

View file

@ -233,9 +233,20 @@ func getSignin(w http.ResponseWriter, r *http.Request) {
errorCode := r.URL.Query().Get("error") errorCode := r.URL.Query().Get("error")
htmlSignin(w, errorCode) htmlSignin(w, errorCode)
} }
func getSignup(w http.ResponseWriter, r *http.Request) { func getSignup(w http.ResponseWriter, r *http.Request) {
htmlSignup(w, r.URL.Path) htmlSignup(w, r.URL.Path)
} }
func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
globalid := chi.URLParam(r, "globalid")
if globalid == "" {
respondError(w, "No globalid provided", nil, http.StatusBadRequest)
return
}
htmlSource(w, r, u, globalid)
}
func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) { func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) {
org_id := chi.URLParam(r, "org_id") org_id := chi.URLParam(r, "org_id")
tileset_id := chi.URLParam(r, "tileset_id") tileset_id := chi.URLParam(r, "tileset_id")

71
html.go
View file

@ -32,6 +32,7 @@ var (
dashboard = newBuiltTemplate("dashboard", "authenticated") dashboard = newBuiltTemplate("dashboard", "authenticated")
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated") oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
settings = newBuiltTemplate("settings", "authenticated") settings = newBuiltTemplate("settings", "authenticated")
source = newBuiltTemplate("source", "authenticated")
) )
// Unauthenticated pages // Unauthenticated pages
@ -57,7 +58,7 @@ var (
) )
var components = [...]string{"header", "map"} var components = [...]string{"header", "map"}
type BreedingSource struct { type BreedingSourceSummary struct {
ID string ID string
Type string Type string
LastInspected *time.Time LastInspected *time.Time
@ -70,17 +71,21 @@ type BuiltTemplate struct {
template *template.Template template *template.Template
} }
type MapMarker struct {
LatLng LatLng
}
type ComponentMap struct { type ComponentMap struct {
Center LatLng Center LatLng
GeoJSON interface{} GeoJSON interface{}
MapboxToken string MapboxToken string
Markers []MapMarker
Zoom int Zoom int
} }
type ContentAuthenticatedPlaceholder struct { type ContentAuthenticatedPlaceholder struct {
User User User User
} }
type ContentCell struct { type ContentCell struct {
BreedingSources []BreedingSource BreedingSources []BreedingSourceSummary
CellBoundary h3.CellBoundary CellBoundary h3.CellBoundary
Inspections []Inspection Inspections []Inspection
MapData ComponentMap MapData ComponentMap
@ -114,6 +119,11 @@ type ContentSignin struct {
InvalidCredentials bool InvalidCredentials bool
} }
type ContentSignup struct{} type ContentSignup struct{}
type ContentSource struct {
MapData ComponentMap
Source *BreedingSourceDetail
User User
}
type LatLng struct { type LatLng struct {
Lat float64 Lat float64
Lng float64 Lng float64
@ -467,6 +477,47 @@ func htmlSignup(w http.ResponseWriter, path string) {
renderOrError(w, signup, data) renderOrError(w, signup, data)
} }
func htmlSource(w http.ResponseWriter, r *http.Request, user *models.User, id string) {
org, err := user.Organization().One(r.Context(), 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
}
data := ContentSource{
MapData: ComponentMap{
Center: LatLng{
Lat: s.GeometryY,
Lng: s.GeometryX,
},
//GeoJSON:
MapboxToken: MapboxToken,
Markers: []MapMarker{
MapMarker{
LatLng: LatLng{
Lat: s.GeometryY,
Lng: s.GeometryX,
},
},
},
Zoom: 13,
},
Source: s,
User: userContent,
}
renderOrError(w, source, data)
}
func gisStatement(cb h3.CellBoundary) string { func gisStatement(cb h3.CellBoundary) string {
var content strings.Builder var content strings.Builder
for i, p := range cb { for i, p := range cb {
@ -671,8 +722,8 @@ func inspectionsByCell(ctx context.Context, org *models.Organization, c h3.Cell)
} }
return results, nil return results, nil
} }
func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSource, error) { func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSourceSummary, error) {
var results []BreedingSource var results []BreedingSourceSummary
boundary, err := c.Boundary() boundary, err := c.Boundary()
if err != nil { if err != nil {
@ -689,7 +740,7 @@ func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.C
return results, fmt.Errorf("Failed to query rows: %w", err) return results, fmt.Errorf("Failed to query rows: %w", err)
} }
for _, r := range rows { for _, r := range rows {
results = append(results, BreedingSource{ results = append(results, BreedingSourceSummary{
ID: r.Globalid, ID: r.Globalid,
LastInspected: fsTimestampToTime(r.Lastinspectdate), LastInspected: fsTimestampToTime(r.Lastinspectdate),
LastTreated: fsTimestampToTime(r.Lasttreatdate), LastTreated: fsTimestampToTime(r.Lasttreatdate),
@ -706,3 +757,13 @@ func uuidShort(uuid string) string {
return uuid[:3] + "..." + uuid[len(uuid)-4:] return uuid[:3] + "..." + uuid[len(uuid)-4:]
} }
func sourceByGlobalId(ctx context.Context, org *models.Organization, id string) (*BreedingSourceDetail, error) {
row, err := org.FSPointlocations(
sm.Where(models.FSPointlocations.Columns.Globalid.EQ(psql.Arg(id))),
).One(ctx, PGInstance.BobDB)
if err != nil {
return nil, fmt.Errorf("Failed to get point location: %w", err)
}
return ConvertToDisplayModel(row), nil
}

View file

@ -113,6 +113,7 @@ func main() {
// Authenticated endpoints // Authenticated endpoints
r.Method("GET", "/cell/{cell}", NewEnsureAuth(getCellDetails)) r.Method("GET", "/cell/{cell}", NewEnsureAuth(getCellDetails))
r.Method("GET", "/settings", NewEnsureAuth(getSettings)) r.Method("GET", "/settings", NewEnsureAuth(getSettings))
r.Method("GET", "/source/{globalid}", NewEnsureAuth(getSource))
r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles)) r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles))
localFS := http.Dir("./static") localFS := http.Dir("./static")

View file

@ -0,0 +1,14 @@
-- +goose Up
ALTER TABLE fs_pointlocation ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API
UPDATE fs_pointlocation SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857);
ALTER TABLE fs_treatment ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API
UPDATE fs_treatment SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857);
ALTER TABLE fs_mosquitoinspection ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API
UPDATE fs_mosquitoinspection SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857);
-- +goose Down
ALTER TABLE fs_pointlocation DROP COLUMN geom;
ALTER TABLE fs_treatment DROP COLUMN geom;
ALTER TABLE fs_mosquitoinspection DROP COLUMN geom;

161
model_conversion.go Normal file
View file

@ -0,0 +1,161 @@
package main
import (
"github.com/Gleipnir-Technology/nidus-sync/models"
"github.com/aarondl/opt/null"
"time"
)
type BreedingSourceDetail struct {
// Basic Information
OrganizationID int32 `json:"organizationId"`
Name string `json:"name"`
Description string `json:"description"`
LocationNumber int64 `json:"locationNumber"`
ObjectID int32 `json:"objectId"`
GlobalID string `json:"globalId"`
ExternalID string `json:"externalId"`
// Status Information
Active bool `json:"active"`
DeactivateReason string `json:"deactivateReason"`
SourceStatus string `json:"sourceStatus"`
Priority string `json:"priority"`
ScalarPriority int64 `json:"scalarPriority"`
// Classification
SourceType string `json:"sourceType"`
Habitat string `json:"habitat"`
UseType string `json:"useType"`
WaterOrigin string `json:"waterOrigin"`
Symbology string `json:"symbology"`
// Geographical Data
X float64 `json:"x"`
Y float64 `json:"y"`
GeometryX float64 `json:"geometryX"`
GeometryY float64 `json:"geometryY"`
Zone string `json:"zone"`
Zone2 string `json:"zone2"`
Jurisdiction string `json:"jurisdiction"`
AccessDescription string `json:"accessDescription"`
// Inspection Data
LarvaeInspectInterval int16 `json:"larvaeInspectInterval"`
LastInspectionDate time.Time `json:"lastInspectionDate"`
LastInspectionActivity string `json:"lastInspectionActivity"`
LastInspectionActionTaken string `json:"lastInspectionActionTaken"`
LastInspectionAverageLarvae float64 `json:"lastInspectionAverageLarvae"`
LastInspectionAveragePupae float64 `json:"lastInspectionAveragePupae"`
LastInspectionBreeding string `json:"lastInspectionBreeding"`
LastInspectionConditions string `json:"lastInspectionConditions"`
LastInspectionFieldSpecies string `json:"lastInspectionFieldSpecies"`
LastInspectionLifeStages string `json:"lastInspectionLifeStages"`
// Treatment Data
LastTreatmentDate time.Time `json:"lastTreatmentDate"`
LastTreatmentActivity string `json:"lastTreatmentActivity"`
LastTreatmentProduct string `json:"lastTreatmentProduct"`
LastTreatmentQuantity float64 `json:"lastTreatmentQuantity"`
LastTreatmentQuantityUnit string `json:"lastTreatmentQuantityUnit"`
// Assignment & Schedule
AssignedTechnician string `json:"assignedTechnician"`
NextActionScheduledDate time.Time `json:"nextActionScheduledDate"`
// Metadata
Created time.Time `json:"created"`
Creator string `json:"creator"`
EditedAt time.Time `json:"editedAt"`
Editor string `json:"editor"`
Updated time.Time `json:"updated"`
Comments string `json:"comments"`
}
// ConvertToDisplayModel transforms the DB model into the display model
func ConvertToDisplayModel(source *models.FSPointlocation) *BreedingSourceDetail {
// Helper function to convert unix timestamp to time.Time
toTime := func(val null.Val[int64]) time.Time {
v, ok := val.Get()
if !ok {
return time.UnixMilli(0)
}
t := time.UnixMilli(v)
return t
}
// Helper function to convert int16 to bool
toBool := func(val null.Val[int16]) bool {
if !val.IsValue() {
return false
}
b := val.MustGet() != 0
return b
}
return &BreedingSourceDetail{
// Basic Information
OrganizationID: source.OrganizationID,
Name: source.Name.MustGet(),
Description: source.Description.MustGet(),
LocationNumber: source.Locationnumber.GetOr(0),
ObjectID: source.Objectid,
GlobalID: source.Globalid,
ExternalID: source.Externalid.GetOr(""),
// Status Information
Active: toBool(source.Active),
DeactivateReason: source.DeactivateReason.GetOr(""),
SourceStatus: source.Sourcestatus.GetOr(""),
Priority: source.Priority.GetOr(""),
ScalarPriority: source.Scalarpriority.GetOr(0),
// Classification
SourceType: source.Stype.GetOr(""),
Habitat: source.Habitat.GetOr(""),
UseType: source.Usetype.GetOr(""),
WaterOrigin: source.Waterorigin.GetOr(""),
Symbology: source.Symbology.GetOr(""),
// Geographical Data
X: source.X.GetOr(0),
Y: source.Y.GetOr(0),
GeometryX: source.GeometryX,
GeometryY: source.GeometryY,
Zone: source.Zone.GetOr(""),
Zone2: source.Zone2.GetOr(""),
Jurisdiction: source.Jurisdiction.GetOr(""),
AccessDescription: source.Accessdesc.GetOr(""),
// Inspection Data
LarvaeInspectInterval: source.Larvinspectinterval.GetOr(0),
LastInspectionDate: toTime(source.Lastinspectdate),
LastInspectionActivity: source.Lastinspectactivity.GetOr(""),
LastInspectionActionTaken: source.Lastinspectactiontaken.GetOr(""),
LastInspectionAverageLarvae: source.Lastinspectavglarvae.GetOr(0),
LastInspectionAveragePupae: source.Lastinspectavgpupae.GetOr(0),
LastInspectionBreeding: source.Lastinspectbreeding.GetOr(""),
LastInspectionConditions: source.Lastinspectconditions.GetOr(""),
LastInspectionFieldSpecies: source.Lastinspectfieldspecies.GetOr(""),
LastInspectionLifeStages: source.Lastinspectlstages.GetOr(""),
// Treatment Data
LastTreatmentDate: toTime(source.Lasttreatdate),
LastTreatmentActivity: source.Lasttreatactivity.GetOr(""),
LastTreatmentProduct: source.Lasttreatproduct.GetOr(""),
LastTreatmentQuantity: source.Lasttreatqty.GetOr(0),
LastTreatmentQuantityUnit: source.Lasttreatqtyunit.GetOr(""),
// Assignment & Schedule
AssignedTechnician: source.Assignedtech.GetOr(""),
NextActionScheduledDate: toTime(source.Nextactiondatescheduled),
// Metadata
Created: toTime(source.Creationdate),
Creator: source.Creator.GetOr(""),
EditedAt: toTime(source.Editdate),
Editor: source.Editor.GetOr(""),
Updated: source.Updated,
Comments: source.Comments.GetOr(""),
}
}

View file

@ -3,6 +3,21 @@
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' /> <link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' />
<script> <script>
const geojson = JSON.parse({{.GeoJSON}}) const geojson = JSON.parse({{.GeoJSON}})
function addMarkers(map, markers) {
for (let i = 0; i < markers.length; i++) {
let marker = markers[i];
marker.addTo(map);
}
}
function mapMarkers() {
const markers = [
{{ range .Markers }}
new mapboxgl.Marker().setLngLat([{{.LatLng.Lng}}, {{.LatLng.Lat}}])
];
{{end}}
return markers;
}
function onLoad() { function onLoad() {
console.log("Setting up the map...", geojson); console.log("Setting up the map...", geojson);
mapboxgl.accessToken = {{ .MapboxToken }}; mapboxgl.accessToken = {{ .MapboxToken }};
@ -14,6 +29,7 @@ function onLoad() {
}); });
map.on("load", function() { map.on("load", function() {
console.log("Map post-load..."); console.log("Map post-load...");
addMarkers(map, mapMarkers());
const sourceId = 'h3-hexes'; const sourceId = 'h3-hexes';
const layerId = 'h3-hexes-layer'; const layerId = 'h3-hexes-layer';
let source = map.getSource(sourceId); let source = map.getSource(sourceId);
@ -36,6 +52,7 @@ function onLoad() {
source = map.getSource(sourceId); source = map.getSource(sourceId);
} }
source.setData(geojson); source.setData(geojson);
console.log("Map post-load done."); console.log("Map post-load done.");
}); });
console.log("Map init done."); console.log("Map init done.");

View file

@ -6,7 +6,7 @@
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' /> <link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' />
<script> <script>
function onLoad() { function onLoad() {
console.log("Setting up the map...", geojson); console.log("Setting up the map...");
mapboxgl.accessToken = {{ .MapData.MapboxToken }}; mapboxgl.accessToken = {{ .MapData.MapboxToken }};
const map = new mapboxgl.Map({ const map = new mapboxgl.Map({
container: 'map', // container ID container: 'map', // container ID

469
templates/source.html Normal file
View file

@ -0,0 +1,469 @@
{{template "authenticated.html" .}}
{{define "title"}}Dash{{end}}
{{define "extraheader"}}
{{template "map" .MapData}}
<style>
.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">
<!-- Source Header Section -->
<div class="row mb-2">
<div class="col-12">
<h1>Breeding Source 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">Source ID: {{ .Source.GlobalID }}</div>
<table class="info-table">
<tr>
<td class="info-label">Access:</td>
<td>{{ .Source.AccessDescription }}</td>
</tr>
<tr>
<td class="info-label">Address:</td>
<td>Not implemented</td>
</tr>
<tr>
<td class="info-label">Comments:</td>
<td>{{ .Source.Comments }}</td>
</tr>
<tr>
<td class="info-label">Deactivate Reason:</td>
<td>{{ .Source.DeactivateReason }}</td>
</tr>
<tr>
<td class="info-label">Description:</td>
<td>{{ .Source.Description }}</td>
</tr>
<tr>
<td class="info-label">Habitat:</td>
<td>{{ .Source.Habitat }}</td>
</tr>
<tr>
<td class="info-label">Jurisdiction:</td>
<td>{{ .Source.Jurisdiction }}</td>
</tr>
<tr>
<td class="info-label">Location Number:</td>
<td>{{ .Source.LocationNumber }}</td>
</tr>
<tr>
<td class="info-label">Name:</td>
<td>{{ .Source.Name }}</td>
</tr>
<tr>
<td>Status</td>
<td>
{{ if .Source.Active }}<span class="badge bg-warning">Active</span>
{{ else }}<span class="badge bg-info">Inactive</span>
{{ end }}
</td>
</tr>
<tr>
<td class="info-label">Priority:</td>
<td>{{ .Source.Priority }} ({{.Source.ScalarPriority}})</td>
</tr>
<tr>
<td class="info-label">S Type:</td>
<td>{{ .Source.SourceType }}</td>
</tr>
<tr>
<td class="info-label">Source Status:</td>
<td>{{ .Source.SourceStatus }}</td>
</tr>
<tr>
<td class="info-label">Symbology:</td>
<td>{{ .Source.Symbology }}</td>
</tr>
<tr>
<td class="info-label">Use Type:</td>
<td>{{ .Source.UseType }}</td>
</tr>
<tr>
<td class="info-label">Water Origin:</td>
<td>{{ .Source.WaterOrigin }}</td>
</tr>
<tr>
<td class="info-label">Zone:</td>
<td>{{ .Source.Zone }}.{{ .Source.Zone2 }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="info-table">
<tr>
<td class="info-label">Creation date</td>
<td>{{ .Source.Created|timeSince }}</td>
</tr>
<tr>
<td class="info-label">Edit date</td>
<td>{{ .Source.EditedAt|timeSince }}</td>
</tr>
<tr>
<td class="info-label">Larva Inspect Interval</td>
<td>{{ .Source.LarvaeInspectInterval }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Activity</td>
<td>{{ .Source.LastInspectionActivity }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Avg Larva</td>
<td>{{ .Source.LastInspectionAverageLarvae }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Avg Pupae</td>
<td>{{ .Source.LastInspectionAveragePupae }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Breeding</td>
<td>{{ .Source.LastInspectionBreeding }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Conditions</td>
<td>{{ .Source.LastInspectionConditions }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Date</td>
<td>{{ .Source.LastInspectionDate|timeSince }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Species</td>
<td>{{ .Source.LastInspectionFieldSpecies }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Life Stages</td>
<td>{{ .Source.LastInspectionLifeStages }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Activity</td>
<td>{{ .Source.LastTreatmentActivity }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Date</td>
<td>{{ .Source.LastTreatmentDate|timeSince }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Product</td>
<td>{{ .Source.LastTreatmentProduct }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Quantity</td>
<td>{{ .Source.LastTreatmentQuantity }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Quantity Unit</td>
<td>{{ .Source.LastTreatmentQuantityUnit }}</td>
</tr>
<tr>
<td class="info-label">Next action date scheduled:</td>
<td>{{ .Source.NextActionScheduledDate|timeSince }}</td>
</tr>
<tr>
<td class="info-label">Updated:</td>
<td>{{ .Source.Updated|timeSince }}</td>
</tr>
<tr>
<td class="info-label">Treatment Cadence:</td>
<td>Not implemented</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>
<!-- Two-Column Layout for Tables -->
<div class="row">
<!-- Left Column -->
<div class="col-md-6">
<!-- Treatments Section -->
<h2 class="section-header">Treatment History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Treatment Date</th>
<th>Insecticide Used</th>
<th>Cadence Delta</th>
<th>Technician Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>04/16/2023</td>
<td>Bacillus thuringiensis israelensis (Bti)</td>
<td class="time-delta-neutral">On time</td>
<td>Applied larvicide to standing water.</td>
</tr>
<tr>
<td>04/02/2023</td>
<td>Methoprene</td>
<td class="time-delta-neutral">On time</td>
<td>Applied to ditch with extended release formula.</td>
</tr>
<tr>
<td>03/18/2023</td>
<td>Bacillus sphaericus</td>
<td class="time-delta-negative">-1 day</td>
<td>Preemptive treatment before rain event.</td>
</tr>
<tr>
<td>03/01/2023</td>
<td>Bacillus thuringiensis israelensis (Bti)</td>
<td class="time-delta-positive">+3 days</td>
<td>Delayed due to weather conditions.</td>
</tr>
<tr>
<td>02/15/2023</td>
<td>Methoprene</td>
<td class="time-delta-neutral">On time</td>
<td>Regular treatment cycle.</td>
</tr>
</tbody>
</table>
</div>
<nav aria-label="Treatments pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- Inspections Section -->
<h2 class="section-header">Inspection History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Inspection Date</th>
<th>Inspector</th>
<th>Larvae Present</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
<tr>
<td>04/15/2023</td>
<td>J. Smith</td>
<td><span class="badge bg-danger">Yes</span></td>
<td>Moderate larvae presence, treatment recommended.</td>
</tr>
<tr>
<td>04/01/2023</td>
<td>M. Johnson</td>
<td><span class="badge bg-danger">Yes</span></td>
<td>High larvae count, immediate treatment needed.</td>
</tr>
<tr>
<td>03/17/2023</td>
<td>J. Smith</td>
<td><span class="badge bg-success">No</span></td>
<td>No larvae observed, previous treatment effective.</td>
</tr>
<tr>
<td>03/03/2023</td>
<td>R. Williams</td>
<td><span class="badge bg-danger">Yes</span></td>
<td>Low larvae count, monitoring recommended.</td>
</tr>
<tr>
<td>02/14/2023</td>
<td>M. Johnson</td>
<td><span class="badge bg-danger">Yes</span></td>
<td>Moderate larvae count, treatment scheduled.</td>
</tr>
</tbody>
</table>
</div>
<nav aria-label="Inspections pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- Right Column -->
<div class="col-md-6">
<!-- Nearby Traps Section -->
<h2 class="section-header">Nearby Mosquito Traps</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Trap ID</th>
<th>Distance</th>
<th>Collection Date</th>
<th>Mosquito Count</th>
</tr>
</thead>
<tbody>
<!-- Trap 1 with multiple collections -->
<tr>
<td rowspan="3">TR-105</td>
<td rowspan="3">0.2 mi</td>
<td>04/17/2023</td>
<td>37</td>
</tr>
<tr>
<td>04/10/2023</td>
<td>52</td>
</tr>
<tr>
<td>04/03/2023</td>
<td>61</td>
</tr>
<!-- Trap 2 with multiple collections -->
<tr>
<td rowspan="3">TR-108</td>
<td rowspan="3">0.4 mi</td>
<td>04/17/2023</td>
<td>22</td>
</tr>
<tr>
<td>04/10/2023</td>
<td>35</td>
</tr>
<tr>
<td>04/03/2023</td>
<td>41</td>
</tr>
<!-- Trap 3 with multiple collections -->
<tr>
<td rowspan="3">TR-112</td>
<td rowspan="3">0.6 mi</td>
<td>04/17/2023</td>
<td>18</td>
</tr>
<tr>
<td>04/10/2023</td>
<td>24</td>
</tr>
<tr>
<td>04/03/2023</td>
<td>27</td>
</tr>
<!-- Trap 4 with multiple collections -->
<tr>
<td rowspan="3">TR-117</td>
<td rowspan="3">0.8 mi</td>
<td>04/17/2023</td>
<td>12</td>
</tr>
<tr>
<td>04/10/2023</td>
<td>19</td>
</tr>
<tr>
<td>04/03/2023</td>
<td>15</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Source Analysis Box -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Source Analysis & Recommendations</h5>
</div>
<div class="card-body">
<p><strong>Risk Level:</strong> <span class="badge bg-danger">High</span></p>
<p><strong>Associated Species:</strong> Culex pipiens, Aedes albopictus</p>
<p><strong>Trend Analysis:</strong> Mosquito counts in nearby traps show a gradual decrease following recent treatments, indicating positive impact of abatement efforts.</p>
<p><strong>Recommendations:</strong></p>
<ul>
<li>Maintain 14-day treatment schedule through summer months</li>
<li>Consider physical modification to reduce standing water retention</li>
<li>Increase trap surveillance in May-June (peak breeding season)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{{end}}