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-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-21 17:51:18 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/config"
|
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-09 21:02:30 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
|
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-01-31 20:08:08 +00:00
|
|
|
//"github.com/rs/zerolog/log"
|
2026-01-21 17:51:18 +00:00
|
|
|
"github.com/stephenafamo/scan"
|
2026-01-09 20:08:29 +00:00
|
|
|
/*
|
2026-01-21 17:51:18 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
|
|
|
|
"github.com/aarondl/opt/omit"
|
|
|
|
|
"github.com/aarondl/opt/omitnull"
|
2026-01-09 20:08:29 +00:00
|
|
|
*/)
|
|
|
|
|
|
2026-01-21 17:51:18 +00:00
|
|
|
type Contact struct {
|
|
|
|
|
Email string
|
|
|
|
|
Name string
|
|
|
|
|
Phone string
|
|
|
|
|
}
|
2026-02-03 17:54:21 +00:00
|
|
|
type ContentStatus struct {
|
|
|
|
|
Error string
|
|
|
|
|
MapboxToken string
|
|
|
|
|
ReportID string
|
|
|
|
|
URL ContentURL
|
|
|
|
|
}
|
|
|
|
|
type ContentStatusByID struct {
|
|
|
|
|
MapboxToken string
|
|
|
|
|
Report Report
|
|
|
|
|
URL ContentURL
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:26:48 +00:00
|
|
|
type Image struct {
|
2026-01-21 21:06:35 +00:00
|
|
|
Location string
|
|
|
|
|
URL string
|
2026-01-21 18:26:48 +00:00
|
|
|
}
|
2026-01-09 20:08:29 +00:00
|
|
|
type Report struct {
|
2026-01-21 17:51:18 +00:00
|
|
|
Address string
|
2026-01-21 18:26:48 +00:00
|
|
|
Comments string
|
2026-01-21 17:51:18 +00:00
|
|
|
Created time.Time
|
2026-01-22 03:27:32 +00:00
|
|
|
District string
|
2026-01-21 17:51:18 +00:00
|
|
|
ID string
|
2026-01-21 18:26:48 +00:00
|
|
|
Images []Image
|
2026-01-21 17:51:18 +00:00
|
|
|
Location string // GeoJSON
|
|
|
|
|
Reporter Contact
|
|
|
|
|
SiteOwner Contact
|
2026-01-21 18:26:48 +00:00
|
|
|
Type string
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
Status = buildTemplate("status", "base")
|
|
|
|
|
StatusByID = buildTemplate("status-by-id", "base")
|
|
|
|
|
)
|
|
|
|
|
|
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{
|
|
|
|
|
Error: "",
|
|
|
|
|
MapboxToken: config.MapboxToken,
|
|
|
|
|
ReportID: "",
|
|
|
|
|
URL: makeContentURL(nil),
|
|
|
|
|
}
|
2026-01-09 21:02:30 +00:00
|
|
|
if report_id_str == "" {
|
2026-02-03 17:54:21 +00:00
|
|
|
html.RenderOrError(w, Status, 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"
|
|
|
|
|
html.RenderOrError(w, Status, content)
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
2026-01-21 17:51:18 +00:00
|
|
|
func contentFromNuisance(ctx context.Context, report_id string) (result ContentStatusByID, err error) {
|
|
|
|
|
nuisance, err := models.PublicreportNuisances.Query(
|
|
|
|
|
models.SelectWhere.PublicreportNuisances.PublicID.EQ(report_id),
|
|
|
|
|
).One(ctx, db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return result, fmt.Errorf("Failed to query nuisance %s: %w", report_id, err)
|
|
|
|
|
}
|
|
|
|
|
result.Report.ID = report_id
|
|
|
|
|
result.Report.Address = nuisance.Address
|
|
|
|
|
result.Report.Created = nuisance.Created
|
2026-01-31 16:14:19 +00:00
|
|
|
result.Report.Reporter.Email = nuisance.ReporterEmail.GetOr("")
|
|
|
|
|
result.Report.Reporter.Name = nuisance.ReporterName.GetOr("")
|
|
|
|
|
result.Report.Reporter.Phone = nuisance.ReporterPhone.GetOr("")
|
2026-01-21 17:51:18 +00:00
|
|
|
|
|
|
|
|
type LocationGeoJSON struct {
|
|
|
|
|
Location string
|
|
|
|
|
}
|
|
|
|
|
row, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
|
|
|
|
|
sm.From(
|
|
|
|
|
psql.F("ST_AsGeoJSON", "location"),
|
|
|
|
|
).As("location"),
|
|
|
|
|
sm.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))),
|
|
|
|
|
), scan.StructMapper[LocationGeoJSON]())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return result, fmt.Errorf("Failed to query nuisance %s: %w", report_id, err)
|
|
|
|
|
}
|
|
|
|
|
result.Report.Location = row.Location
|
|
|
|
|
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
func contentFromPool(ctx context.Context, report_id string) (result ContentStatusByID, err error) {
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
func contentFromQuick(ctx context.Context, report_id string) (result ContentStatusByID, err error) {
|
|
|
|
|
quick, err := models.PublicreportQuicks.Query(
|
|
|
|
|
models.SelectWhere.PublicreportQuicks.PublicID.EQ(report_id),
|
|
|
|
|
).One(ctx, db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return result, fmt.Errorf("Failed to query nuisance %s: %w", report_id, err)
|
|
|
|
|
}
|
2026-01-21 18:26:48 +00:00
|
|
|
|
2026-01-21 21:06:35 +00:00
|
|
|
images, err := sql.PublicreportImageWithJSONByQuickID(quick.ID).All(ctx, db.PGInstance.BobDB)
|
2026-01-21 18:26:48 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return result, fmt.Errorf("Failed to get images %s: %w", report_id, err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 17:51:18 +00:00
|
|
|
result.Report.ID = report_id
|
|
|
|
|
result.Report.Address = quick.Address
|
2026-01-21 18:26:48 +00:00
|
|
|
result.Report.Comments = quick.Comments
|
2026-01-21 17:51:18 +00:00
|
|
|
result.Report.Created = quick.Created
|
2026-01-22 03:27:32 +00:00
|
|
|
result.Report.District = "Unknown"
|
2026-01-21 17:51:18 +00:00
|
|
|
result.Report.Reporter.Email = quick.ReporterEmail
|
|
|
|
|
result.Report.Reporter.Name = "-"
|
|
|
|
|
result.Report.Reporter.Phone = quick.ReporterPhone
|
2026-01-21 18:26:48 +00:00
|
|
|
result.Report.Type = "Quick"
|
2026-01-21 17:51:18 +00:00
|
|
|
|
2026-01-21 18:26:48 +00:00
|
|
|
for _, image := range images {
|
|
|
|
|
result.Report.Images = append(result.Report.Images, Image{
|
2026-01-21 21:06:35 +00:00
|
|
|
Location: image.LocationJSON,
|
2026-01-23 02:50:25 +00:00
|
|
|
URL: config.MakeURLReport("/image/%s", image.StorageUUID.String()),
|
2026-01-21 18:26:48 +00:00
|
|
|
})
|
|
|
|
|
}
|
2026-01-21 17:51:18 +00:00
|
|
|
type LocationGeoJSON struct {
|
|
|
|
|
Location string
|
|
|
|
|
}
|
2026-01-21 21:06:35 +00:00
|
|
|
location, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
|
2026-01-21 17:51:18 +00:00
|
|
|
sm.Columns(
|
|
|
|
|
psql.F("ST_AsGeoJSON", "location"),
|
|
|
|
|
),
|
|
|
|
|
sm.From("publicreport.quick"),
|
|
|
|
|
sm.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))),
|
2026-01-21 21:06:35 +00:00
|
|
|
), scan.SingleColumnMapper[string])
|
2026-01-21 17:51:18 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return result, fmt.Errorf("Failed to query nuisance %s: %w", report_id, err)
|
|
|
|
|
}
|
2026-01-21 21:06:35 +00:00
|
|
|
result.Report.Location = location
|
2026-01-21 17:51:18 +00:00
|
|
|
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
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()
|
|
|
|
|
|
|
|
|
|
location, err := models.PublicreportReportLocations.Query(
|
|
|
|
|
models.SelectWhere.PublicreportReportLocations.PublicID.EQ(report_id),
|
|
|
|
|
).One(ctx, db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to find report", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var content ContentStatusByID
|
|
|
|
|
switch location.TableName.MustGet() {
|
|
|
|
|
case "nuisance":
|
|
|
|
|
content, err = contentFromNuisance(ctx, report_id)
|
|
|
|
|
case "pool":
|
|
|
|
|
content, err = contentFromPool(ctx, report_id)
|
|
|
|
|
case "quick":
|
|
|
|
|
content, err = contentFromQuick(ctx, report_id)
|
|
|
|
|
}
|
|
|
|
|
content.MapboxToken = config.MapboxToken
|
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,
|
|
|
|
|
StatusByID,
|
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 getQuick(w http.ResponseWriter, r *http.Request) {
|
2026-01-30 18:21:27 +00:00
|
|
|
html.RenderOrError(
|
2026-01-09 21:02:30 +00:00
|
|
|
w,
|
|
|
|
|
Quick,
|
2026-01-21 17:51:18 +00:00
|
|
|
ContentQuick{},
|
2026-01-09 21:02:30 +00:00
|
|
|
)
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-09 21:02:30 +00:00
|
|
|
func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
report := r.URL.Query().Get("report")
|
2026-01-30 18:21:27 +00:00
|
|
|
html.RenderOrError(
|
2026-01-09 21:02:30 +00:00
|
|
|
w,
|
|
|
|
|
QuickSubmitComplete,
|
2026-01-21 17:51:18 +00:00
|
|
|
ContentQuickSubmitComplete{
|
2026-01-09 21:02:30 +00:00
|
|
|
ReportID: report,
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-01-09 20:08:29 +00:00
|
|
|
}
|
2026-01-09 21:02:30 +00:00
|
|
|
|
|
|
|
|
func postQuick(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
lat := r.FormValue("latitude")
|
|
|
|
|
lng := r.FormValue("longitude")
|
|
|
|
|
comments := r.FormValue("comments")
|
|
|
|
|
//photos := r.FormValue("photos")
|
|
|
|
|
|
|
|
|
|
latitude, err := strconv.ParseFloat(lat, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create parse latitude", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
longitude, err := strconv.ParseFloat(lng, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create parse longitude", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
u, err := GenerateReportID()
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create quick report public ID", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c, err := h3utils.GetCell(longitude, latitude, 15)
|
|
|
|
|
setter := models.PublicreportQuickSetter{
|
|
|
|
|
Created: omit.From(time.Now()),
|
|
|
|
|
Comments: omit.From(comments),
|
|
|
|
|
//Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)),
|
|
|
|
|
H3cell: omitnull.From(c.String()),
|
|
|
|
|
PublicID: omit.From(u),
|
|
|
|
|
ReporterEmail: omit.From(""),
|
|
|
|
|
ReporterPhone: omit.From(""),
|
|
|
|
|
}
|
|
|
|
|
quick, err := models.PublicreportQuicks.Insert(&setter).One(r.Context(), db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create database record", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = psql.Update(
|
|
|
|
|
um.Table("publicreport.quick"),
|
|
|
|
|
um.SetCol("location").To(fmt.Sprintf("ST_GeometryFromText('Point(%f %f)')", longitude, latitude)),
|
|
|
|
|
um.Where(psql.Quote("id").EQ(psql.Arg(quick.ID))),
|
|
|
|
|
).Exec(r.Context(), db.PGInstance.BobDB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to insert publicreport", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
log.Info().Float64("latitude", latitude).Float64("longitude", longitude).Msg("Got upload")
|
|
|
|
|
photoSetters := make([]*models.PublicreportQuickPhotoSetter, 0)
|
|
|
|
|
uploads, err := extractPhotoUploads(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to extract photo uploads", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, u := range uploads {
|
|
|
|
|
photoSetters = append(photoSetters, &models.PublicreportQuickPhotoSetter{
|
|
|
|
|
Filename: omit.From(u.Filename),
|
|
|
|
|
Size: omit.From(u.Size),
|
|
|
|
|
UUID: omit.From(u.UUID),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
err = quick.InsertQuickPhotos(r.Context(), db.PGInstance.BobDB, photoSetters...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to create photo records", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/quick-submit-complete?report=%s", u), http.StatusFound)
|
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)
|
|
|
|
|
}
|