2026-01-29 23:55:41 +00:00
|
|
|
package rmo
|
2026-01-09 20:08:29 +00:00
|
|
|
|
|
|
|
|
import (
|
2026-01-21 17:51:18 +00:00
|
|
|
"context"
|
2026-01-09 21:02:30 +00:00
|
|
|
"fmt"
|
2026-01-09 20:08:29 +00:00
|
|
|
"net/http"
|
2026-02-05 02:24:37 +00:00
|
|
|
"strconv"
|
2026-01-09 21:02:30 +00:00
|
|
|
"strings"
|
2026-01-21 17:51:18 +00:00
|
|
|
"time"
|
2026-01-09 20:08:29 +00:00
|
|
|
|
2026-01-27 18:44:02 +00:00
|
|
|
"github.com/Gleipnir-Technology/bob"
|
|
|
|
|
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
|
|
|
|
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
|
2026-01-09 21:02:30 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
2026-01-21 17:51:18 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
2026-01-30 18:21:27 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/html"
|
2026-01-09 20:08:29 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2026-03-18 15:36:20 +00:00
|
|
|
"github.com/google/uuid"
|
2026-03-11 14:59:04 +00:00
|
|
|
//"github.com/rs/zerolog/log"
|
2026-01-21 17:51:18 +00:00
|
|
|
"github.com/stephenafamo/scan"
|
2026-03-18 15:36:20 +00:00
|
|
|
"golang.org/x/text/cases"
|
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
)
|
2026-01-09 20:08:29 +00:00
|
|
|
|
2026-02-03 17:54:21 +00:00
|
|
|
type ContentStatus struct {
|
2026-03-09 18:02:22 +00:00
|
|
|
District *ContentDistrict
|
|
|
|
|
Error string
|
|
|
|
|
ReportID string
|
|
|
|
|
URL ContentURL
|
2026-02-03 17:54:21 +00:00
|
|
|
}
|
|
|
|
|
type ContentStatusByID struct {
|
2026-03-09 18:02:22 +00:00
|
|
|
District *ContentDistrict
|
|
|
|
|
Report Report
|
|
|
|
|
Timeline []TimelineEntry
|
|
|
|
|
URL ContentURL
|
2026-02-03 17:54:21 +00:00
|
|
|
}
|
2026-02-05 02:24:37 +00:00
|
|
|
type DetailEntry struct {
|
|
|
|
|
Name string
|
|
|
|
|
Value string
|
|
|
|
|
}
|
2026-01-09 20:08:29 +00:00
|
|
|
type Report struct {
|
2026-02-05 02:36:24 +00:00
|
|
|
Address string
|
|
|
|
|
Comments string
|
|
|
|
|
Created time.Time
|
|
|
|
|
Details []DetailEntry
|
|
|
|
|
ID string
|
|
|
|
|
ImageCount int
|
|
|
|
|
Location string // GeoJSON
|
|
|
|
|
Status string
|
|
|
|
|
Type string
|
|
|
|
|
}
|
|
|
|
|
type TimelineEntry struct {
|
|
|
|
|
At time.Time
|
|
|
|
|
Detail string
|
|
|
|
|
Title string
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-09 21:02:30 +00:00
|
|
|
func formatReportID(s string) string {
|
|
|
|
|
// truncate down if too long
|
|
|
|
|
if len(s) > 12 {
|
|
|
|
|
s = s[:12]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If less than 4 characters, return as is
|
|
|
|
|
if len(s) < 4 {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If at least 8 characters, add hyphens at positions 4 and 8
|
|
|
|
|
if len(s) >= 8 {
|
|
|
|
|
return s[0:4] + "-" + s[4:8] + "-" + s[8:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If at least 4 characters but less than 8, add hyphen only at position 4
|
|
|
|
|
return s[0:4] + "-" + s[4:]
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 20:08:29 +00:00
|
|
|
func getStatus(w http.ResponseWriter, r *http.Request) {
|
2026-01-09 21:02:30 +00:00
|
|
|
report_id_str := r.URL.Query().Get("report")
|
2026-02-03 17:54:21 +00:00
|
|
|
content := ContentStatus{
|
2026-03-09 18:02:22 +00:00
|
|
|
Error: "",
|
|
|
|
|
ReportID: "",
|
|
|
|
|
URL: makeContentURL(nil),
|
2026-02-03 17:54:21 +00:00
|
|
|
}
|
2026-01-09 21:02:30 +00:00
|
|
|
if report_id_str == "" {
|
2026-02-07 05:51:21 +00:00
|
|
|
html.RenderOrError(w, "rmo/status.html", content)
|
2026-01-09 21:02:30 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
report_id := sanitizeReportID(report_id_str)
|
|
|
|
|
report_id_str = formatReportID(report_id)
|
2026-01-31 20:08:08 +00:00
|
|
|
//some_report, e := report.FindSomeReport(r.Context(), report_id)
|
2026-02-03 17:54:21 +00:00
|
|
|
content.Error = "Sorry, we can't find that report"
|
2026-02-07 05:51:21 +00:00
|
|
|
html.RenderOrError(w, "rmo/status.html", content)
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
func contentFromReport(ctx context.Context, report *models.PublicreportReport) (result ContentStatusByID, err error) {
|
|
|
|
|
org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, report.OrganizationID)
|
2026-01-21 17:51:18 +00:00
|
|
|
if err != nil {
|
2026-03-18 15:36:20 +00:00
|
|
|
return result, fmt.Errorf("Failed to get district information: %w", err)
|
2026-01-21 17:51:18 +00:00
|
|
|
}
|
2026-02-05 02:36:24 +00:00
|
|
|
|
2026-03-18 15:36:20 +00:00
|
|
|
type _Row struct {
|
|
|
|
|
ID int32 `db:"id"`
|
|
|
|
|
ContentType string `db:"content_type"`
|
|
|
|
|
Created time.Time `db:"created"`
|
|
|
|
|
Location string `db:"location"`
|
|
|
|
|
LocationJSON string `db:"location_json"`
|
|
|
|
|
ResolutionX int32 `db:"resolution_x"`
|
|
|
|
|
ResolutionY int32 `db:"resolution_y"`
|
|
|
|
|
StorageUUID uuid.UUID `db:"storage_uuid"`
|
|
|
|
|
StorageSize int32 `db:"storage_size"`
|
|
|
|
|
UploadedFilename string `db:"uploaded_filename"`
|
2026-02-05 02:36:24 +00:00
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
images, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select(
|
|
|
|
|
sm.Columns(
|
|
|
|
|
"id",
|
|
|
|
|
"content_type",
|
|
|
|
|
"created",
|
|
|
|
|
"location",
|
|
|
|
|
"COALESCE(ST_AsGeoJSON(location), '{}') AS location_json",
|
|
|
|
|
"resolution_x",
|
|
|
|
|
"resolution_y",
|
|
|
|
|
"storage_uuid",
|
|
|
|
|
"storage_size",
|
|
|
|
|
"uploaded_filename",
|
|
|
|
|
),
|
|
|
|
|
sm.From("publicreport.image"),
|
|
|
|
|
sm.InnerJoin("publicreport.report_image").OnEQ(
|
|
|
|
|
psql.Quote("publicreport", "image", "id"),
|
|
|
|
|
psql.Quote("publicreport", "report_image", "image_id"),
|
|
|
|
|
),
|
|
|
|
|
sm.Where(
|
|
|
|
|
psql.Quote("publicreport", "report_image", "report_id").EQ(psql.Arg(report.ID)),
|
|
|
|
|
),
|
|
|
|
|
), scan.StructMapper[_Row]())
|
2026-03-14 15:53:16 +00:00
|
|
|
if err != nil {
|
2026-03-18 15:36:20 +00:00
|
|
|
return result, fmt.Errorf("Failed to get images: %w", err)
|
2026-02-10 14:55:59 +00:00
|
|
|
}
|
2026-03-14 15:53:16 +00:00
|
|
|
result.District = newContentDistrict(org)
|
2026-03-18 15:36:20 +00:00
|
|
|
result.Report.ID = report.PublicID
|
|
|
|
|
result.Report.Address = report.AddressRaw
|
|
|
|
|
result.Report.Created = report.Created
|
2026-02-05 02:36:24 +00:00
|
|
|
result.Report.ImageCount = len(images)
|
2026-03-18 15:36:20 +00:00
|
|
|
result.Report.Status = cases.Title(language.AmericanEnglish).String(report.Status.String())
|
|
|
|
|
result.Timeline = []TimelineEntry{
|
|
|
|
|
TimelineEntry{
|
|
|
|
|
At: report.Created,
|
|
|
|
|
Detail: "Initial report was submitted",
|
|
|
|
|
Title: "Created",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LocationGeoJSON struct {
|
|
|
|
|
Location string
|
|
|
|
|
}
|
|
|
|
|
location, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
|
|
|
|
|
sm.Columns(
|
|
|
|
|
psql.F("ST_AsGeoJSON", "location"),
|
|
|
|
|
),
|
|
|
|
|
sm.From("publicreport.report"),
|
|
|
|
|
sm.Where(psql.Quote("id").EQ(psql.Arg(report.ID))),
|
|
|
|
|
), scan.SingleColumnMapper[string])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return result, fmt.Errorf("Failed to query location of report %d: %w", report.ID, err)
|
|
|
|
|
}
|
|
|
|
|
result.Report.Location = location
|
|
|
|
|
nuisance, err := models.PublicreportNuisances.Query(
|
|
|
|
|
models.SelectWhere.PublicreportNuisances.ReportID.EQ(report.ID),
|
|
|
|
|
).One(ctx, db.PGInstance.BobDB)
|
|
|
|
|
if err == nil {
|
|
|
|
|
result.Report.Type = "Mosquito Nuisance"
|
|
|
|
|
addContentFromNuisance(&result, nuisance)
|
|
|
|
|
}
|
|
|
|
|
water, err := models.PublicreportWaters.Query(
|
|
|
|
|
models.SelectWhere.PublicreportWaters.ReportID.EQ(report.ID),
|
|
|
|
|
).One(ctx, db.PGInstance.BobDB)
|
|
|
|
|
if err == nil {
|
|
|
|
|
result.Report.Type = "Standing Water"
|
|
|
|
|
addContentFromWater(&result, water)
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
func addContentFromNuisance(result *ContentStatusByID, nuisance *models.PublicreportNuisance) {
|
2026-02-05 02:49:23 +00:00
|
|
|
result.Report.Type = "Mosquito Nuisance"
|
2026-02-05 02:24:37 +00:00
|
|
|
result.Report.Details = []DetailEntry{
|
|
|
|
|
DetailEntry{
|
2026-02-05 16:56:36 +00:00
|
|
|
Name: "Active early morning (5a-8a)?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.TodEarly),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active daytime (8a-5p)?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.TodDay),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active evening (5p-9p)?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.TodEvening),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active night (9p-5a)?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.TodNight),
|
2026-02-05 02:24:37 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Duration",
|
|
|
|
|
Value: nuisance.Duration.String(),
|
|
|
|
|
},
|
2026-02-05 16:56:36 +00:00
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active in backyard?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.IsLocationBackyard),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active in frontyard?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.IsLocationFrontyard),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active in garden?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.IsLocationGarden),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active in other location?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.IsLocationOther),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Active in pool area?",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.IsLocationPool),
|
|
|
|
|
},
|
2026-02-05 02:24:37 +00:00
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Stagnant Water",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.SourceStagnant),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Container",
|
|
|
|
|
Value: strconv.FormatBool(nuisance.SourceContainer),
|
|
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Sprinklers & Gutters",
|
2026-02-05 16:56:36 +00:00
|
|
|
Value: strconv.FormatBool(nuisance.SourceGutter),
|
2026-02-05 02:24:37 +00:00
|
|
|
},
|
|
|
|
|
}
|
2026-01-21 17:51:18 +00:00
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
func addContentFromWater(result *ContentStatusByID, water *models.PublicreportWater) {
|
2026-02-05 21:43:29 +00:00
|
|
|
result.Report.Details = []DetailEntry{
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Has a gate that affects access?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.AccessGate),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Has dog that affects access?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.AccessDog),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Has a fence that affects access?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.AccessFence),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Has a locked entrace that affects access?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.AccessLocked),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Reporter observed larvae (wigglers)?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.HasLarvae),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Reporter observed pupae (tumblers)?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.HasPupae),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
DetailEntry{
|
|
|
|
|
Name: "Reporter observed adult mosquitoes?",
|
2026-03-09 22:59:21 +00:00
|
|
|
Value: strconv.FormatBool(water.HasAdult),
|
2026-02-05 21:43:29 +00:00
|
|
|
},
|
|
|
|
|
}
|
2026-01-21 17:51:18 +00:00
|
|
|
}
|
2026-02-05 21:43:29 +00:00
|
|
|
|
2026-01-09 20:08:29 +00:00
|
|
|
func getStatusByID(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
report_id := chi.URLParam(r, "report_id")
|
2026-01-21 17:51:18 +00:00
|
|
|
ctx := r.Context()
|
|
|
|
|
|
2026-03-18 15:36:20 +00:00
|
|
|
report, err := models.PublicreportReports.Query(
|
|
|
|
|
models.SelectWhere.PublicreportReports.PublicID.EQ(report_id),
|
2026-01-21 17:51:18 +00:00
|
|
|
).One(ctx, db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to find report", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
content, err := contentFromReport(ctx, report)
|
2026-02-05 21:43:29 +00:00
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to generate report content", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-03 17:54:21 +00:00
|
|
|
content.URL = makeContentURL(nil)
|
2026-01-30 18:21:27 +00:00
|
|
|
html.RenderOrError(
|
2026-01-09 20:08:29 +00:00
|
|
|
w,
|
2026-02-07 05:51:21 +00:00
|
|
|
"rmo/status-by-id.html",
|
2026-01-21 17:51:18 +00:00
|
|
|
content,
|
2026-01-09 20:08:29 +00:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 21:02:30 +00:00
|
|
|
func sanitizeReportID(r string) string {
|
|
|
|
|
result := ""
|
|
|
|
|
for _, char := range r {
|
|
|
|
|
if char != '-' {
|
|
|
|
|
result += string(char)
|
|
|
|
|
}
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
2026-01-09 21:02:30 +00:00
|
|
|
return strings.ToUpper(result)
|
|
|
|
|
}
|