From f3221ec31563432fae83ccee790ccb4e697e06bc Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 4 Feb 2026 16:07:36 +0000 Subject: [PATCH] Add report ID suggestion to status page --- .../publicreport_publicid_suggestion.bob.go | 117 ++++++++++++++++++ .../publicreport_publicid_suggestion.bob.sql | 25 ++++ db/sql/publicreport_publicid_suggestion.sql | 22 ++++ .../static/js/address-or-report-suggestion.js | 115 +++++++++++++---- rmo/report.go | 64 ++++++++++ rmo/routes.go | 1 + 6 files changed, 317 insertions(+), 27 deletions(-) create mode 100644 db/sql/publicreport_publicid_suggestion.bob.go create mode 100644 db/sql/publicreport_publicid_suggestion.bob.sql create mode 100644 db/sql/publicreport_publicid_suggestion.sql create mode 100644 rmo/report.go diff --git a/db/sql/publicreport_publicid_suggestion.bob.go b/db/sql/publicreport_publicid_suggestion.bob.go new file mode 100644 index 00000000..23515444 --- /dev/null +++ b/db/sql/publicreport_publicid_suggestion.bob.go @@ -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) +} diff --git a/db/sql/publicreport_publicid_suggestion.bob.sql b/db/sql/publicreport_publicid_suggestion.bob.sql new file mode 100644 index 00000000..75de3064 --- /dev/null +++ b/db/sql/publicreport_publicid_suggestion.bob.sql @@ -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; diff --git a/db/sql/publicreport_publicid_suggestion.sql b/db/sql/publicreport_publicid_suggestion.sql new file mode 100644 index 00000000..a81f07fe --- /dev/null +++ b/db/sql/publicreport_publicid_suggestion.sql @@ -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; diff --git a/html/static/js/address-or-report-suggestion.js b/html/static/js/address-or-report-suggestion.js index 7e8b995b..7ee9cb29 100644 --- a/html/static/js/address-or-report-suggestion.js +++ b/html/static/js/address-or-report-suggestion.js @@ -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 ` +
+
${formatted_id}
+
${type_display}
+
` + }).join(""); + const addressElements = addresses.map((item, index) => { if (item.properties.place_formatted != "") { return `
+ data-lng="${item.geometry.coordinates[0]}" + data-type="address">
${item.properties.name || item.properties.full_address}
${item.properties.place_formatted}
` @@ -115,27 +150,36 @@ class AddressOrReportInput extends HTMLElement {
+ data-lng="${item.coordinates.lng}" + data-type="address">
${item.properties.name || item.properties.full_address}
${item.properties.place_formatted}
` } - }).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"; } } diff --git a/rmo/report.go b/rmo/report.go new file mode 100644 index 00000000..1afc3aee --- /dev/null +++ b/rmo/report.go @@ -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) +} diff --git a/rmo/routes.go b/rmo/routes.go index ddb231ea..cf997873 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -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)