Add report ID suggestion to status page

This commit is contained in:
Eli Ribble 2026-02-04 16:07:36 +00:00
parent 7032f8e26b
commit f3221ec315
No known key found for this signature in database
6 changed files with 317 additions and 27 deletions

View file

@ -0,0 +1,117 @@
// Code generated by BobGen psql v0.42.5. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package sql
import (
"context"
_ "embed"
"io"
"iter"
"github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/bob/clause"
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/dialect"
"github.com/Gleipnir-Technology/bob/orm"
"github.com/aarondl/opt/null"
"github.com/stephenafamo/scan"
)
//go:embed publicreport_publicid_suggestion.bob.sql
var formattedQueries_publicreport_publicid_suggestion string
var publicreportPublicIDSuggestionSQL = formattedQueries_publicreport_publicid_suggestion[168:426]
type PublicreportPublicIDSuggestionQuery = orm.ModQuery[*dialect.SelectQuery, publicreportPublicIDSuggestion, PublicreportPublicIDSuggestionRow, []PublicreportPublicIDSuggestionRow, publicreportPublicIDSuggestionTransformer]
func PublicreportPublicIDSuggestion(Arg1 string) *PublicreportPublicIDSuggestionQuery {
var expressionTypArgs publicreportPublicIDSuggestion
expressionTypArgs.Arg1 = psql.Arg(Arg1)
return &PublicreportPublicIDSuggestionQuery{
Query: orm.Query[publicreportPublicIDSuggestion, PublicreportPublicIDSuggestionRow, []PublicreportPublicIDSuggestionRow, publicreportPublicIDSuggestionTransformer]{
ExecQuery: orm.ExecQuery[publicreportPublicIDSuggestion]{
BaseQuery: bob.BaseQuery[publicreportPublicIDSuggestion]{
Expression: expressionTypArgs,
Dialect: dialect.Dialect,
QueryType: bob.QueryTypeSelect,
},
},
Scanner: func(context.Context, []string) (func(*scan.Row) (any, error), func(any) (PublicreportPublicIDSuggestionRow, error)) {
return func(row *scan.Row) (any, error) {
var t PublicreportPublicIDSuggestionRow
row.ScheduleScanByIndex(0, &t.TableName)
row.ScheduleScanByIndex(1, &t.PublicID)
row.ScheduleScanByIndex(2, &t.Location)
return &t, nil
}, func(v any) (PublicreportPublicIDSuggestionRow, error) {
return *(v.(*PublicreportPublicIDSuggestionRow)), nil
}
},
},
Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) {
q.AppendSelect(expressionTypArgs.subExpr(10, 59))
q.SetTable(expressionTypArgs.subExpr(68, 89))
q.AppendWhere(expressionTypArgs.subExpr(99, 116))
q.AppendCombine(clause.Combine{
Strategy: "UNION",
All: true,
Query: bob.BaseQuery[bob.Expression]{
Expression: expressionTypArgs.subExpr(129, 237),
QueryType: bob.QueryTypeSelect,
Dialect: dialect.Dialect,
},
})
q.CombinedOrder.AppendOrder(expressionTypArgs.subExpr(249, 258))
}),
}
}
type PublicreportPublicIDSuggestionRow = struct {
TableName string `db:"table_name"`
PublicID string `db:"public_id"`
Location null.Val[string] `db:"location"`
}
type publicreportPublicIDSuggestionTransformer = bob.SliceTransformer[PublicreportPublicIDSuggestionRow, []PublicreportPublicIDSuggestionRow]
type publicreportPublicIDSuggestion struct {
Arg1 bob.Expression
}
func (o publicreportPublicIDSuggestion) args() iter.Seq[orm.ArgWithPosition] {
return func(yield func(arg orm.ArgWithPosition) bool) {
if !yield(orm.ArgWithPosition{
Name: "arg1",
Start: 114,
Stop: 116,
Expression: o.Arg1,
}) {
return
}
if !yield(orm.ArgWithPosition{
Name: "arg1",
Start: 235,
Stop: 237,
Expression: o.Arg1,
}) {
return
}
}
}
func (o publicreportPublicIDSuggestion) raw(from, to int) string {
return publicreportPublicIDSuggestionSQL[from:to]
}
func (o publicreportPublicIDSuggestion) subExpr(from, to int) bob.Expression {
return orm.ArgsToExpression(publicreportPublicIDSuggestionSQL, from, to, o.args())
}
func (o publicreportPublicIDSuggestion) WriteSQL(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
return o.subExpr(0, len(publicreportPublicIDSuggestionSQL)).WriteSQL(ctx, w, d, start)
}

View file

@ -0,0 +1,25 @@
-- Code generated by BobGen psql v0.42.5. DO NOT EDIT.
-- This file is meant to be re-generated in place and/or deleted at any time.
-- PublicreportPublicIDSuggestion
SELECT
'nuisance' AS table_name,
public_id,
location
FROM
publicreport.nuisance
WHERE
public_id LIKE $1
UNION ALL
SELECT
'pool' AS table_name,
public_id,
location
FROM
publicreport.pool
WHERE
public_id LIKE $2
ORDER BY
public_id;

View file

@ -0,0 +1,22 @@
-- PublicreportPublicIDSuggestion
SELECT
'nuisance' AS table_name,
public_id,
location
FROM
publicreport.nuisance
WHERE
public_id LIKE $1
UNION ALL
SELECT
'pool' AS table_name,
public_id,
location
FROM
publicreport.pool
WHERE
public_id LIKE $1
ORDER BY
public_id;

View file

@ -10,8 +10,10 @@ class AddressOrReportInput extends HTMLElement {
this.render();
// Element references
this._addresses = [];
this._input = this.shadowRoot.querySelector("input");
this._suggestions = this.shadowRoot.querySelector(".suggestions-container");
this._reports = [];
this._suggestionsContainer = this.shadowRoot.querySelector(".suggestions-container");
// Bind methods
this._handleInput = this._handleInput.bind(this);
@ -72,16 +74,13 @@ class AddressOrReportInput extends HTMLElement {
// Clear suggestions if input is less than 3 characters
if (searchText.length < 3) {
this._suggestions.innerHTML = '';
this._suggestionsContainer.innerHTML = '';
return;
}
// Debounce API calls (wait 300ms after typing stops)
this._debounceTimer = setTimeout(() => {
this._fetchAddressSuggestions(searchText)
.then(response => {
this._renderSuggestions(response.features);
});
this._handleSuggestions(searchText);
}, 300);
}
@ -92,21 +91,57 @@ class AddressOrReportInput extends HTMLElement {
const response = await fetch(url);
const data = await response.json();
return data;
return data.features;
} catch (error) {
console.error('Error fetching geocoding suggestions:', error);
}
}
_renderSuggestions(suggestions) {
console.log("Rendering suggestions", suggestions);
this._suggestions.innerHTML = suggestions.map((item, index) => {
async _fetchReportSuggestions(text) {
try {
const url = `/report/suggest?r=${text}`
const response = await fetch(url);
const data = await response.json();
return data.reports;
} catch (error) {
console.error("Error fetching report suggestions:", error);
}
}
async _handleSuggestions(text) {
await Promise.all([
(async() => {
this._addresses = await this._fetchAddressSuggestions(text);
})(),
(async() => {
this._reports = await this._fetchReportSuggestions(text);
})(),
]);
this._renderSuggestions(this._addresses, this._reports);
}
_renderSuggestions(addresses, reports) {
console.log("Rendering suggestions", addresses, reports);
const reportElements = reports.map((item, index) => {
const formatted_id = _formatReportID(item.id);
const type_display = _formatReportType(item.type);
return `
<div class="suggestion-item list-group-item"
data-index="${index}"
data-report-id="${item.id}"
data-type="report">
<div class="report-id">${formatted_id}</div>
<div class="report-type">${type_display}</div>
</div>`
}).join("");
const addressElements = addresses.map((item, index) => {
if (item.properties.place_formatted != "") {
return `
<div class="suggestion-item list-group-item"
data-index="${index}"
data-lat="${item.geometry.coordinates[1]}"
data-lng="${item.geometry.coordinates[0]}">
data-lng="${item.geometry.coordinates[0]}"
data-type="address">
<div class="main-address">${item.properties.name || item.properties.full_address}</div>
<div class="place-info">${item.properties.place_formatted}</div>
</div>`
@ -115,27 +150,36 @@ class AddressOrReportInput extends HTMLElement {
<div class="suggestion-item list-group-item"
data-index="${index}"
data-lat="${item.coordinates.lat}"
data-lng="${item.coordinates.lng}">
data-lng="${item.coordinates.lng}"
data-type="address">
<div class="main-address">${item.properties.name || item.properties.full_address}</div>
<div class="place-info">${item.properties.place_formatted}</div>
</div>`
}
}).join('');
}).join("");
this._suggestionsContainer.innerHTML = reportElements + addressElements;
// Add click listeners to suggestions
this.shadowRoot.querySelectorAll('.suggestion-item').forEach(el => {
el.addEventListener('click', e => {
const index = parseInt(el.dataset.index);
const suggestion = suggestions[index];
this.SetValue(suggestion);
// Dispatch custom event
this.dispatchEvent(new CustomEvent('address-selected', {
bubbles: true,
composed: true, // Allows event to cross shadow DOM boundary
detail: {
location: suggestion
}
}));
const type = el.dataset.type;
if (type == "report") {
const index = parseInt(el.dataset.index);
const report = this._reports[index];
this.value = _formatReportID(report.id);
this._suggestionsContainer.innerHTML = "";
} else if (type == "address") {
const index = parseInt(el.dataset.index);
const address = this._addresses[index];
this.SetValue(suggestion);
// Dispatch custom event
this.dispatchEvent(new CustomEvent('address-selected', {
bubbles: true,
composed: true, // Allows event to cross shadow DOM boundary
detail: {
location: suggestion
}
}));
}
});
});
}
@ -197,13 +241,30 @@ class AddressOrReportInput extends HTMLElement {
clear() {
if (this._input) {
this._input.value = '';
this._suggestions.innerHTML = '';
this._suggestionsContainer.innerHTML = '';
}
}
SetValue(suggestion) {
this.value = suggestion.properties.full_address;
this._suggestions.innerHTML = '';
this._suggestionsContainer.innerHTML = '';
}
}
function _formatReportID(id) {
if (id.length === 12) {
return `${id.substring(0, 4)}-${id.substring(4, 8)}-${id.substring(8)}`;
}
return id;
}
function _formatReportType(type) {
if (type == "nuisance") {
return "Mosquito Nuisance Report";
} else if (type == "pool") {
return "Standing Water Report";
} else {
return "Unknown Report Type";
}
}

64
rmo/report.go Normal file
View file

@ -0,0 +1,64 @@
package rmo
import (
"encoding/json"
"net/http"
//"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
//"github.com/go-chi/chi/v5"
//"github.com/rs/zerolog/log"
)
type ReportSuggestion struct {
ID string `json:"id"`
Type string `json:"type"`
//Location string
}
type ReportSuggestionResponse struct {
Reports []ReportSuggestion `json:"reports"`
}
func getReportSuggestion(w http.ResponseWriter, r *http.Request) {
partial_report_id := r.FormValue("r")
if partial_report_id == "" {
respondError(w, "You need at least a bit of an 'r'", nil, http.StatusBadRequest)
return
}
p := partial_report_id + "%"
ctx := r.Context()
rows, err := sql.PublicreportPublicIDSuggestion(p).All(ctx, db.PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to query DB: %w", err, http.StatusInternalServerError)
return
}
var result ReportSuggestionResponse
for _, row := range rows {
/*
value, err := row.Location.Value()
if err != nil {
log.Warn().Err(err).Msg("Failed to get value")
continue
}
value_str, ok := value.(string)
if !ok {
log.Warn().Msg("Failed to get location as string")
continue
}
log.Debug().Str("location", value_str).Msg("Looking at row")
*/
result.Reports = append(result.Reports, ReportSuggestion{
Type: row.TableName,
ID: row.PublicID,
//Location: "",
})
}
jsonBody, err := json.Marshal(result)
if err != nil {
respondError(w, "Failed to marshal JSON: %w", err, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonBody)
}

View file

@ -38,6 +38,7 @@ func Router() chi.Router {
r.Get("/quick-submit-complete", getQuickSubmitComplete)
r.Post("/register-notifications", postRegisterNotifications)
r.Get("/register-notifications-complete", getRegisterNotificationsComplete)
r.Get("/report/suggest", getReportSuggestion)
r.Get("/search", getSearch)
r.Get("/scss/*", getScssDebug)
r.Get("/status", getStatus)