diff --git a/api/lead.go b/api/lead.go
index d274133f..af64a5ac 100644
--- a/api/lead.go
+++ b/api/lead.go
@@ -3,27 +3,14 @@ package api
import (
"context"
"net/http"
- "time"
- "github.com/Gleipnir-Technology/bob"
- "github.com/Gleipnir-Technology/bob/dialect/psql"
- "github.com/Gleipnir-Technology/bob/dialect/psql/sm"
- "github.com/Gleipnir-Technology/bob/dialect/psql/um"
- "github.com/Gleipnir-Technology/nidus-sync/db"
- "github.com/Gleipnir-Technology/nidus-sync/db/enums"
- "github.com/Gleipnir-Technology/nidus-sync/db/models"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
- "github.com/Gleipnir-Technology/nidus-sync/platform/geom"
- "github.com/aarondl/opt/omit"
- "github.com/aarondl/opt/omitnull"
- "github.com/rs/zerolog/log"
- "github.com/stephenafamo/scan"
)
type createLead struct {
- PoolLocations map[int]Location `json:"pool_locations"`
- SignalIDs []int `json:"signal_ids"`
+ PoolLocations map[int]platform.Location `json:"pool_locations"`
+ SignalIDs []int `json:"signal_ids"`
}
type createdLead struct {
ID int32 `json:"id"`
@@ -48,75 +35,21 @@ func postLeads(ctx context.Context, r *http.Request, user platform.User, req cre
return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet")
}
signal_id := req.SignalIDs[0]
- txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
- defer txn.Rollback(ctx)
-
- if err != nil {
- return nil, nhttp.NewError("start transaction: %w", err)
- }
- type _Row struct {
- ID int32 `db:"site_id"`
- }
- site, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
- sm.Columns(
- "pool.site_id AS site_id",
- ),
- sm.From("signal_pool"),
- sm.InnerJoin("pool").OnEQ(
- psql.Quote("signal_pool", "pool_id"),
- psql.Quote("pool", "id"),
- ),
- sm.InnerJoin("site").On(
- psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")),
- ),
- sm.Where(psql.Quote("signal_pool", "signal_id").EQ(psql.Arg(signal_id))),
- sm.Where(psql.Quote("site", "organization_id").EQ(psql.Arg(user.Organization.ID()))),
- ), scan.StructMapper[_Row]())
- if err != nil {
- if err.Error() == "sql: no rows in result set" {
- return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Can't make a lead from signal %d: %w", signal_id, err)
- }
- return nil, nhttp.NewError("failed getting site: %w", err)
- }
-
- lead, err := models.Leads.Insert(&models.LeadSetter{
- Created: omit.From(time.Now()),
- Creator: omit.From(int32(user.ID)),
- // ID
- OrganizationID: omit.From(int32(user.Organization.ID())),
- SiteID: omitnull.From(site.ID),
- Type: omit.From(enums.LeadtypeGreenPool),
- }).One(ctx, txn)
- if err != nil {
- return nil, nhttp.NewError("failed to create lead: %w", err)
- }
- _, err = psql.Update(
- um.Table("signal"),
- um.SetCol("addressed").ToArg(time.Now()),
- um.SetCol("addressor").ToArg(user.ID),
- um.Where(psql.Quote("id").EQ(psql.Arg(signal_id))),
- ).Exec(ctx, txn)
- if err != nil {
- return nil, nhttp.NewError("failed to update signal %d: %w", signal_id, err)
- }
- pool_location, ok := req.PoolLocations[signal_id]
+ var pool_location *platform.Location
+ l, ok := req.PoolLocations[signal_id]
if ok {
- log.Info().Float64("lat", pool_location.Latitude).Float64("lng", pool_location.Longitude).Msg("got pool location")
- geom_query := geom.PostgisPointQuery(pool_location.Longitude, pool_location.Latitude)
- _, err = psql.Update(
- um.Table("pool"),
- um.SetCol("geometry").To(geom_query),
- um.From("signal_pool"),
- um.Where(psql.Quote("signal_pool", "pool_id").EQ(psql.Quote("pool", "id"))),
- um.Where(psql.Quote("signal_pool", "signal_id").EQ(psql.Arg(signal_id))),
- ).Exec(ctx, txn)
- if err != nil {
- return nil, nhttp.NewError("failed to update pool through signal %d: %w", signal_id, err)
- }
+ pool_location = &l
+ }
+ site_id, err := platform.SiteFromSignal(ctx, user, int32(signal_id))
+ if err != nil || site_id == nil {
+ return nil, nhttp.NewError("site from signal: %w", err)
+ }
+ lead_id, err := platform.LeadCreate(ctx, user, int32(signal_id), *site_id, pool_location)
+ if err != nil || lead_id == nil {
+ return nil, nhttp.NewError("lead create: %w", err)
}
- txn.Commit(ctx)
return &createdLead{
- ID: lead.ID,
+ ID: *lead_id,
}, nil
}
diff --git a/api/publicreport.go b/api/publicreport.go
new file mode 100644
index 00000000..9df70d0a
--- /dev/null
+++ b/api/publicreport.go
@@ -0,0 +1,41 @@
+package api
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/Gleipnir-Technology/nidus-sync/config"
+ nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
+ "github.com/Gleipnir-Technology/nidus-sync/platform"
+)
+
+type formPublicreportLead struct {
+ ReportID string `json:"reportID"`
+}
+
+func postPublicreportLead(ctx context.Context, r *http.Request, user platform.User, req formPublicreportLead) (*createdLead, *nhttp.ErrorWithStatus) {
+ lead_id, err := platform.LeadCreateFromPublicreport(ctx, user, req.ReportID)
+ if err != nil {
+ return nil, nhttp.NewError("create lead: %w", err)
+ }
+ return &createdLead{
+ ID: *lead_id,
+ }, nil
+}
+
+type formPublicreportInvalid struct {
+ ReportID string `json:"reportID"`
+}
+type createdReport struct {
+ URI string `json:"uri"`
+}
+
+func postPublicreportInvalid(ctx context.Context, r *http.Request, user platform.User, req formPublicreportLead) (*createdReport, *nhttp.ErrorWithStatus) {
+ err := platform.PublicreportInvalid(ctx, user, req.ReportID)
+ if err != nil {
+ return nil, nhttp.NewError("create lead: %w", err)
+ }
+ return &createdReport{
+ URI: config.MakeURLNidus("/publicreport/%s", req.ReportID),
+ }, nil
+}
diff --git a/api/review_task.go b/api/review_task.go
index 8085b35e..2e192cb8 100644
--- a/api/review_task.go
+++ b/api/review_task.go
@@ -22,7 +22,7 @@ type reviewTaskPool struct {
Created time.Time `json:"created"`
Creator platform.User `json:"creator"`
ID int32 `json:"id"`
- Location Location `json:"location"`
+ Location types.Location `json:"location"`
Reviewed *time.Time `json:"addressed"`
Reviewer *platform.User `json:"addressor"`
}
@@ -122,7 +122,7 @@ func listReviewTaskPool(ctx context.Context, r *http.Request, user platform.User
Created: row.Created,
Creator: *users_by_id[row.CreatorID],
ID: row.ID,
- Location: Location{
+ Location: types.Location{
Latitude: row.Latitude,
Longitude: row.Longitude,
},
diff --git a/api/routes.go b/api/routes.go
index 49f5f521..01dcbb01 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -21,6 +21,8 @@ func AddRoutes(r chi.Router) {
r.Method("GET", "/leads", authenticatedHandlerJSON(listLead))
r.Method("POST", "/leads", authenticatedHandlerJSONPost(postLeads))
r.Method("GET", "/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource))
+ r.Method("POST", "/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid))
+ r.Method("POST", "/publicreport/lead", authenticatedHandlerJSONPost(postPublicreportLead))
r.Method("POST", "/review/pool", authenticatedHandlerJSONPost(postReviewPool))
r.Method("GET", "/review-task/pool", authenticatedHandlerJSON(listReviewTaskPool))
r.Method("GET", "/service-request", auth.NewEnsureAuth(apiServiceRequest))
diff --git a/api/signal.go b/api/signal.go
index ad0ccde4..6c900849 100644
--- a/api/signal.go
+++ b/api/signal.go
@@ -23,7 +23,7 @@ type signal struct {
Created time.Time `json:"created"`
Creator platform.User `json:"creator"`
ID int32 `json:"id"`
- Location Location `json:"location"`
+ Location types.Location `json:"location"`
Species string `json:"species"`
Title string `json:"title"`
Type string `json:"type"`
@@ -34,18 +34,18 @@ type contentListSignal struct {
func listSignal(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListSignal, *nhttp.ErrorWithStatus) {
type _Row struct {
- Address types.Address `db:"address"`
- Addressed *time.Time `db:"addressed"`
- Addressor *int32 `db:"addressor"`
- Created time.Time `db:"created"`
- Creator int32 `db:"creator_id"`
- ID int32 `db:"id"`
- Latitude float64 `db:"latitude"`
- Longitude float64 `db:"longitude"`
- Location Location `db:"location"`
- Species *string `db:"species"`
- Title string `db:"title"`
- Type string `db:"type"`
+ Address types.Address `db:"address"`
+ Addressed *time.Time `db:"addressed"`
+ Addressor *int32 `db:"addressor"`
+ Created time.Time `db:"created"`
+ Creator int32 `db:"creator_id"`
+ ID int32 `db:"id"`
+ Latitude float64 `db:"latitude"`
+ Longitude float64 `db:"longitude"`
+ Location types.Location `db:"location"`
+ Species *string `db:"species"`
+ Title string `db:"title"`
+ Type string `db:"type"`
}
limit := 20
if query.Limit != nil {
@@ -118,7 +118,7 @@ func listSignal(ctx context.Context, r *http.Request, user platform.User, query
Created: row.Created,
Creator: *users_by_id[row.Creator],
ID: row.ID,
- Location: Location{
+ Location: types.Location{
Latitude: row.Latitude,
Longitude: row.Longitude,
},
diff --git a/api/types.go b/api/types.go
index 29e9256d..eebc2855 100644
--- a/api/types.go
+++ b/api/types.go
@@ -34,11 +34,6 @@ func NewBounds() Bounds {
}
}
-type Location struct {
- Latitude float64 `json:"latitude"`
- Longitude float64 `json:"longitude"`
-}
-
type NoteImagePayload struct {
UUID string `json:"uuid"`
Cell H3Cell `json:"cell"`
diff --git a/db/dbinfo/publicreport.report_location.bob.go b/db/dbinfo/publicreport.report_location.bob.go
index f61aa376..398c63b0 100644
--- a/db/dbinfo/publicreport.report_location.bob.go
+++ b/db/dbinfo/publicreport.report_location.bob.go
@@ -31,6 +31,15 @@ var PublicreportReportLocations = Table[
Generated: false,
AutoIncr: false,
},
+ AddressID: column{
+ Name: "address_id",
+ DBType: "integer",
+ Default: "NULL",
+ Comment: "",
+ Nullable: true,
+ Generated: false,
+ AutoIncr: false,
+ },
AddressRaw: column{
Name: "address_raw",
DBType: "text",
@@ -58,6 +67,33 @@ var PublicreportReportLocations = Table[
Generated: false,
AutoIncr: false,
},
+ LocationLatitude: column{
+ Name: "location_latitude",
+ DBType: "double precision",
+ Default: "NULL",
+ Comment: "",
+ Nullable: true,
+ Generated: false,
+ AutoIncr: false,
+ },
+ LocationLongitude: column{
+ Name: "location_longitude",
+ DBType: "double precision",
+ Default: "NULL",
+ Comment: "",
+ Nullable: true,
+ Generated: false,
+ AutoIncr: false,
+ },
+ OrganizationID: column{
+ Name: "organization_id",
+ DBType: "integer",
+ Default: "NULL",
+ Comment: "",
+ Nullable: true,
+ Generated: false,
+ AutoIncr: false,
+ },
PublicID: column{
Name: "public_id",
DBType: "text",
@@ -82,18 +118,22 @@ var PublicreportReportLocations = Table[
}
type publicreportReportLocationColumns struct {
- ID column
- TableName column
- AddressRaw column
- Created column
- Location column
- PublicID column
- Status column
+ ID column
+ TableName column
+ AddressID column
+ AddressRaw column
+ Created column
+ Location column
+ LocationLatitude column
+ LocationLongitude column
+ OrganizationID column
+ PublicID column
+ Status column
}
func (c publicreportReportLocationColumns) AsSlice() []column {
return []column{
- c.ID, c.TableName, c.AddressRaw, c.Created, c.Location, c.PublicID, c.Status,
+ c.ID, c.TableName, c.AddressID, c.AddressRaw, c.Created, c.Location, c.LocationLatitude, c.LocationLongitude, c.OrganizationID, c.PublicID, c.Status,
}
}
diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go
index 3a3bebb1..bf23bbd6 100644
--- a/db/enums/enums.bob.go
+++ b/db/enums/enums.bob.go
@@ -1294,12 +1294,18 @@ func (e *Imagedatatype) Scan(value any) error {
// Enum values for Leadtype
const (
- LeadtypeGreenPool Leadtype = "green-pool"
+ LeadtypeUnknown Leadtype = "unknown"
+ LeadtypeGreenPool Leadtype = "green-pool"
+ LeadtypePublicreportNuisance Leadtype = "publicreport-nuisance"
+ LeadtypePublicreportWater Leadtype = "publicreport-water"
)
func AllLeadtype() []Leadtype {
return []Leadtype{
+ LeadtypeUnknown,
LeadtypeGreenPool,
+ LeadtypePublicreportNuisance,
+ LeadtypePublicreportWater,
}
}
@@ -1311,7 +1317,10 @@ func (e Leadtype) String() string {
func (e Leadtype) Valid() bool {
switch e {
- case LeadtypeGreenPool:
+ case LeadtypeUnknown,
+ LeadtypeGreenPool,
+ LeadtypePublicreportNuisance,
+ LeadtypePublicreportWater:
return true
default:
return false
diff --git a/db/migrations/00106_publicreport_location_org_id.sql b/db/migrations/00106_publicreport_location_org_id.sql
new file mode 100644
index 00000000..e109120c
--- /dev/null
+++ b/db/migrations/00106_publicreport_location_org_id.sql
@@ -0,0 +1,72 @@
+-- +goose Up
+DROP VIEW publicreport.report_location;
+CREATE VIEW publicreport.report_location AS
+SELECT
+ ROW_NUMBER() OVER (ORDER BY table_name, public_id) AS id,
+ table_name,
+ address_id,
+ address_raw,
+ created,
+ location,
+ location_latitude,
+ location_longitude,
+ organization_id,
+ public_id,
+ status
+FROM (
+ SELECT
+ 'nuisance' AS table_name,
+ address_id,
+ address_raw,
+ created,
+ location,
+ ST_X(location) AS location_longitude,
+ ST_Y(location) AS location_latitude,
+ organization_id,
+ public_id,
+ status
+ FROM publicreport.nuisance
+ UNION
+ SELECT
+ 'water' AS table_name,
+ address_id,
+ address_raw,
+ created,
+ location,
+ ST_X(location) AS location_longitude,
+ ST_Y(location) AS location_latitude,
+ organization_id,
+ public_id,
+ status
+ FROM publicreport.water
+) AS combined_data;
+-- +goose Down
+DROP VIEW publicreport.report_location;
+CREATE VIEW publicreport.report_location AS
+SELECT
+ ROW_NUMBER() OVER (ORDER BY table_name, public_id) AS id,
+ table_name,
+ address_raw,
+ created,
+ location,
+ public_id,
+ status
+FROM (
+ SELECT
+ 'nuisance' AS table_name,
+ address_raw,
+ created,
+ location,
+ public_id,
+ status
+ FROM publicreport.nuisance
+ UNION
+ SELECT
+ 'water' AS table_name,
+ address_raw,
+ created,
+ location,
+ public_id,
+ status
+ FROM publicreport.water
+) AS combined_data;
diff --git a/db/migrations/00107_lead_type_publicreport.sql b/db/migrations/00107_lead_type_publicreport.sql
new file mode 100644
index 00000000..09422230
--- /dev/null
+++ b/db/migrations/00107_lead_type_publicreport.sql
@@ -0,0 +1,4 @@
+-- +goose Up
+ALTER TYPE LeadType ADD VALUE 'unknown' BEFORE 'green-pool';
+ALTER TYPE LeadType ADD VALUE 'publicreport-nuisance' AFTER 'green-pool';
+ALTER TYPE LeadType ADD VALUE 'publicreport-water' AFTER 'publicreport-nuisance';
diff --git a/db/models/publicreport.report_location.bob.go b/db/models/publicreport.report_location.bob.go
index a2512c1f..d6f268fe 100644
--- a/db/models/publicreport.report_location.bob.go
+++ b/db/models/publicreport.report_location.bob.go
@@ -16,13 +16,17 @@ import (
// PublicreportReportLocation is an object representing the database table.
type PublicreportReportLocation struct {
- ID null.Val[int64] `db:"id" `
- TableName null.Val[string] `db:"table_name" `
- AddressRaw null.Val[string] `db:"address_raw" `
- Created null.Val[time.Time] `db:"created" `
- Location null.Val[string] `db:"location" `
- PublicID null.Val[string] `db:"public_id" `
- Status null.Val[enums.PublicreportReportstatustype] `db:"status" `
+ ID null.Val[int64] `db:"id" `
+ TableName null.Val[string] `db:"table_name" `
+ AddressID null.Val[int32] `db:"address_id" `
+ AddressRaw null.Val[string] `db:"address_raw" `
+ Created null.Val[time.Time] `db:"created" `
+ Location null.Val[string] `db:"location" `
+ LocationLatitude null.Val[float64] `db:"location_latitude" `
+ LocationLongitude null.Val[float64] `db:"location_longitude" `
+ OrganizationID null.Val[int32] `db:"organization_id" `
+ PublicID null.Val[string] `db:"public_id" `
+ Status null.Val[enums.PublicreportReportstatustype] `db:"status" `
}
// PublicreportReportLocationSlice is an alias for a slice of pointers to PublicreportReportLocation.
@@ -38,29 +42,37 @@ type PublicreportReportLocationsQuery = *psql.ViewQuery[*PublicreportReportLocat
func buildPublicreportReportLocationColumns(alias string) publicreportReportLocationColumns {
return publicreportReportLocationColumns{
ColumnsExpr: expr.NewColumnsExpr(
- "id", "table_name", "address_raw", "created", "location", "public_id", "status",
+ "id", "table_name", "address_id", "address_raw", "created", "location", "location_latitude", "location_longitude", "organization_id", "public_id", "status",
).WithParent("publicreport.report_location"),
- tableAlias: alias,
- ID: psql.Quote(alias, "id"),
- TableName: psql.Quote(alias, "table_name"),
- AddressRaw: psql.Quote(alias, "address_raw"),
- Created: psql.Quote(alias, "created"),
- Location: psql.Quote(alias, "location"),
- PublicID: psql.Quote(alias, "public_id"),
- Status: psql.Quote(alias, "status"),
+ tableAlias: alias,
+ ID: psql.Quote(alias, "id"),
+ TableName: psql.Quote(alias, "table_name"),
+ AddressID: psql.Quote(alias, "address_id"),
+ AddressRaw: psql.Quote(alias, "address_raw"),
+ Created: psql.Quote(alias, "created"),
+ Location: psql.Quote(alias, "location"),
+ LocationLatitude: psql.Quote(alias, "location_latitude"),
+ LocationLongitude: psql.Quote(alias, "location_longitude"),
+ OrganizationID: psql.Quote(alias, "organization_id"),
+ PublicID: psql.Quote(alias, "public_id"),
+ Status: psql.Quote(alias, "status"),
}
}
type publicreportReportLocationColumns struct {
expr.ColumnsExpr
- tableAlias string
- ID psql.Expression
- TableName psql.Expression
- AddressRaw psql.Expression
- Created psql.Expression
- Location psql.Expression
- PublicID psql.Expression
- Status psql.Expression
+ tableAlias string
+ ID psql.Expression
+ TableName psql.Expression
+ AddressID psql.Expression
+ AddressRaw psql.Expression
+ Created psql.Expression
+ Location psql.Expression
+ LocationLatitude psql.Expression
+ LocationLongitude psql.Expression
+ OrganizationID psql.Expression
+ PublicID psql.Expression
+ Status psql.Expression
}
func (c publicreportReportLocationColumns) Alias() string {
@@ -96,13 +108,17 @@ func (o PublicreportReportLocationSlice) AfterQueryHook(ctx context.Context, exe
}
type publicreportReportLocationWhere[Q psql.Filterable] struct {
- ID psql.WhereNullMod[Q, int64]
- TableName psql.WhereNullMod[Q, string]
- AddressRaw psql.WhereNullMod[Q, string]
- Created psql.WhereNullMod[Q, time.Time]
- Location psql.WhereNullMod[Q, string]
- PublicID psql.WhereNullMod[Q, string]
- Status psql.WhereNullMod[Q, enums.PublicreportReportstatustype]
+ ID psql.WhereNullMod[Q, int64]
+ TableName psql.WhereNullMod[Q, string]
+ AddressID psql.WhereNullMod[Q, int32]
+ AddressRaw psql.WhereNullMod[Q, string]
+ Created psql.WhereNullMod[Q, time.Time]
+ Location psql.WhereNullMod[Q, string]
+ LocationLatitude psql.WhereNullMod[Q, float64]
+ LocationLongitude psql.WhereNullMod[Q, float64]
+ OrganizationID psql.WhereNullMod[Q, int32]
+ PublicID psql.WhereNullMod[Q, string]
+ Status psql.WhereNullMod[Q, enums.PublicreportReportstatustype]
}
func (publicreportReportLocationWhere[Q]) AliasedAs(alias string) publicreportReportLocationWhere[Q] {
@@ -111,12 +127,16 @@ func (publicreportReportLocationWhere[Q]) AliasedAs(alias string) publicreportRe
func buildPublicreportReportLocationWhere[Q psql.Filterable](cols publicreportReportLocationColumns) publicreportReportLocationWhere[Q] {
return publicreportReportLocationWhere[Q]{
- ID: psql.WhereNull[Q, int64](cols.ID),
- TableName: psql.WhereNull[Q, string](cols.TableName),
- AddressRaw: psql.WhereNull[Q, string](cols.AddressRaw),
- Created: psql.WhereNull[Q, time.Time](cols.Created),
- Location: psql.WhereNull[Q, string](cols.Location),
- PublicID: psql.WhereNull[Q, string](cols.PublicID),
- Status: psql.WhereNull[Q, enums.PublicreportReportstatustype](cols.Status),
+ ID: psql.WhereNull[Q, int64](cols.ID),
+ TableName: psql.WhereNull[Q, string](cols.TableName),
+ AddressID: psql.WhereNull[Q, int32](cols.AddressID),
+ AddressRaw: psql.WhereNull[Q, string](cols.AddressRaw),
+ Created: psql.WhereNull[Q, time.Time](cols.Created),
+ Location: psql.WhereNull[Q, string](cols.Location),
+ LocationLatitude: psql.WhereNull[Q, float64](cols.LocationLatitude),
+ LocationLongitude: psql.WhereNull[Q, float64](cols.LocationLongitude),
+ OrganizationID: psql.WhereNull[Q, int32](cols.OrganizationID),
+ PublicID: psql.WhereNull[Q, string](cols.PublicID),
+ Status: psql.WhereNull[Q, enums.PublicreportReportstatustype](cols.Status),
}
}
diff --git a/html/template/sync/communication-root.html b/html/template/sync/communication-root.html
index 33d78d3b..ce53ca26 100644
--- a/html/template/sync/communication-root.html
+++ b/html/template/sync/communication-root.html
@@ -45,10 +45,7 @@
typeFilter: "all",
messageText: "",
showPhotoModal: false,
- showInvalidModal: false,
currentPhotoIndex: 0,
- invalidReason: "",
- invalidNotes: "",
showToast: false,
toastTitle: "",
toastMessage: "",
@@ -82,7 +79,7 @@
});
},
- async loadCommunications() {
+ async fetchCommunications() {
try {
// Build query parameters from filters
const params = new URLSearchParams();
@@ -108,7 +105,7 @@
this.loading = true;
this.error = null;
try {
- await Promise.all([this.loadCommunications()]);
+ await Promise.all([this.fetchCommunications()]);
} catch (err) {
this.error = err.message;
console.error("Error loading data:", err);
@@ -143,54 +140,66 @@
}
},
- createLead() {
- // TODO: Implement API call to create lead
- console.log(
- "Creating lead for report:",
- this.selectedCommunication.id,
- );
-
- // Add to activity log
- if (!this.selectedCommunication.history) {
- this.selectedCommunication.history = [];
+ async createLead() {
+ try {
+ const payload = {
+ reportID: this.selectedCommunication.id,
+ };
+ const response = await fetch(`api/publicreport/lead`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+ if (!response.ok) {
+ throw new Error("Failed to submit lead");
+ }
+ // Remove from list after creating lead
+ this.removeCurrentFromList();
+ this.fetchCommunications();
+ } catch (err) {
+ this.error = err.message;
+ console.error("Error creating lead:", err);
}
- this.selectedCommunication.history.push({
- action: "Lead created",
- timestamp: new Date(),
- });
-
- this.showNotification(
- "Lead Created",
- `Lead successfully created for report #${this.selectedCommunication.id}`,
- );
-
- // Remove from list after creating lead
- // this.communications = this.communications.filter(r => r.id !== this.selectedCommunication.id);
- // this.selectedCommunication = null;
},
- markInvalid() {
- // TODO: Implement API call to mark as invalid
+ async markInvalid() {
console.log(
"Marking report as invalid:",
this.selectedCommunication.id,
- this.invalidReason,
- this.invalidNotes,
);
+ const payload = {
+ reportID: this.selectedCommunication.id,
+ };
+ const response = await fetch(`api/publicreport/invalid`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
this.showNotification(
"Report Marked Invalid",
- `Report #${this.selectedCommunication.id} has been marked as ${this.invalidReason}`,
+ `Report #${this.selectedCommunication.id} has been marked as invalid`,
);
-
- // Remove from list
- this.communications = this.communications.filter(
- (r) => r.id !== this.selectedCommunication.id,
+ this.removeCurrentFromList();
+ this.fetchCommunications();
+ },
+ removeCurrentFromList() {
+ const index = this.communications.findIndex(
+ (c) => c.id === this.selectedCommunication.id,
);
- this.selectedCommunication = null;
- this.showInvalidModal = false;
- this.invalidReason = "";
- this.invalidNotes = "";
+ if (index > -1) {
+ this.communications = this.communications.splice(index, 1);
+ }
+ if (this.communications.length > 0) {
+ const nextIndex = Math.min(index, this.communications.length - 1);
+ this.selectedCommunication = this.communications[nextIndex];
+ } else {
+ this.selectedCommunication = null;
+ }
},
sendMessage() {
@@ -744,10 +753,7 @@
-
-
-
-
-
diff --git a/platform/csv/flyover.go b/platform/csv/flyover.go
index 474a0419..1f63a48e 100644
--- a/platform/csv/flyover.go
+++ b/platform/csv/flyover.go
@@ -18,6 +18,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
"github.com/Gleipnir-Technology/nidus-sync/platform/geom"
+ "github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/rs/zerolog/log"
@@ -214,7 +215,10 @@ func insertFlyover(ctx context.Context, txn bob.Tx, file *models.FileuploadFile,
if err != nil {
return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", lng, lat)
}
- geom_query := geom.PostgisPointQuery(lng, lat)
+ geom_query := geom.PostgisPointQuery(types.Location{
+ Latitude: lat,
+ Longitude: lng,
+ })
_, err = psql.Update(
um.TableAs("fileupload.pool", "pool"),
um.SetCol("h3cell").ToArg(cell),
diff --git a/platform/csv/pool.go b/platform/csv/pool.go
index 40511076..fd4f3d5f 100644
--- a/platform/csv/pool.go
+++ b/platform/csv/pool.go
@@ -139,11 +139,11 @@ func geocodePool(ctx context.Context, txn bob.Tx, client *stadia.StadiaMaps, job
PostalCode: pool.AddressPostalCode,
Street: pool.AddressStreet,
}
- address, err := geocode.Geocode(ctx, job.org, a)
+ address, err := geocode.GeocodeStructured(ctx, job.org, a)
if err != nil {
addError(ctx, txn, job.csv, job.rownumber, 0, err.Error())
}
- geom_query := geom.PostgisPointQuery(address.Longitude, address.Latitude)
+ geom_query := geom.PostgisPointQuery(address.Location)
_, err = psql.Update(
um.Table("fileupload.pool"),
um.SetCol("h3cell").ToArg(address.Cell),
diff --git a/platform/geocode/geocode.go b/platform/geocode/geocode.go
index 1d42e9a8..943faaec 100644
--- a/platform/geocode/geocode.go
+++ b/platform/geocode/geocode.go
@@ -20,10 +20,9 @@ import (
)
type GeocodeResult struct {
- Address types.Address
- Cell h3.Cell
- Longitude float64
- Latitude float64
+ Address types.Address
+ Cell h3.Cell
+ Location types.Location
}
var client *stadia.StadiaMaps
@@ -105,7 +104,7 @@ func EnsureAddressWithGeocode(ctx context.Context, txn bob.Tx, org *models.Organ
return address, nil
}
// Geocode
- geo, err := Geocode(ctx, org, a)
+ geo, err := GeocodeStructured(ctx, org, a)
if err != nil {
return nil, fmt.Errorf("geocode: %w", err)
}
@@ -122,7 +121,7 @@ func EnsureAddressWithGeocode(ctx context.Context, txn bob.Tx, org *models.Organ
psql.Arg(geo.Cell),
psql.Raw("DEFAULT"),
psql.Arg(geo.Address.Locality),
- psql.F("ST_Point", geo.Longitude, geo.Latitude, 4326),
+ psql.F("ST_Point", geo.Location.Longitude, geo.Location.Latitude, 4326),
psql.Arg(geo.Address.Number),
psql.Arg(geo.Address.PostalCode),
psql.Arg(geo.Address.Region),
@@ -149,51 +148,66 @@ func EnsureAddressWithGeocode(ctx context.Context, txn bob.Tx, org *models.Organ
Number: geo.Address.Number,
}, nil
}
-
-func Geocode(ctx context.Context, org *models.Organization, a types.Address) (GeocodeResult, error) {
+func GeocodeRaw(ctx context.Context, org *models.Organization, address string) (*GeocodeResult, error) {
+ req := stadia.RequestGeocodeRaw{
+ Text: address,
+ }
+ maybeAddServiceArea(&req, org)
+ resp, err := client.GeocodeRaw(ctx, req)
+ if err != nil {
+ return nil, fmt.Errorf("client raw geocode failure on %s: %w", address, err)
+ }
+ return toGeocodeResult(*resp, address)
+}
+func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Address) (*GeocodeResult, error) {
street := fmt.Sprintf("%s %s", a.Number, a.Street)
- country_s := a.Country
- /*
- sublog := log.With().
- Str("street", street).
- Str("country", country).
- Str("locality", a.Locality).
- Str("postal", a.PostalCode).
- Str("region", a.Region).
- Logger()
- */
- req := stadia.StructuredGeocodeRequest{
- Address: &street,
- Country: &country_s,
+ req := stadia.RequestGeocodeStructured{
+ Address: &street,
+ //Country: &a.Country,
Locality: &a.Locality,
PostalCode: &a.PostalCode,
Region: &a.Region,
}
maybeAddServiceArea(&req, org)
- resp, err := client.StructuredGeocode(ctx, req)
+ resp, err := client.GeocodeStructured(ctx, req)
if err != nil {
- return GeocodeResult{}, fmt.Errorf("client structured geocode failure on %s: %w", a.String(), err)
+ return nil, fmt.Errorf("client structured geocode failure on %s: %w", a.String(), err)
}
+ return toGeocodeResult(*resp, a.String())
+}
+func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResult, error) {
+ req := stadia.RequestReverseGeocode{
+ Latitude: location.Latitude,
+ Longitude: location.Longitude,
+ }
+ resp, err := client.ReverseGeocode(ctx, req)
+ if err != nil {
+ return nil, fmt.Errorf("client reverse geocode failure on %s: %w", location.String(), err)
+ }
+ return toGeocodeResult(*resp, location.String())
+
+}
+func toGeocodeResult(resp stadia.GeocodeResponse, address string) (*GeocodeResult, error) {
if len(resp.Features) < 1 {
- return GeocodeResult{}, fmt.Errorf("%s matched no locations", a.String())
+ return nil, fmt.Errorf("%s matched no locations", address)
}
feature := resp.Features[0]
if len(resp.Features) > 1 {
if !allFeaturesIdenticalEnough(resp.Features) {
- return GeocodeResult{}, fmt.Errorf("%s matched more than one location, and they differ a lot", a.String())
+ return nil, fmt.Errorf("%s matched more than one location, and they differ a lot", address)
}
}
if feature.Geometry.Type != "Point" {
- return GeocodeResult{}, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, a.String())
+ return nil, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, address)
}
longitude := feature.Geometry.Coordinates[0]
latitude := feature.Geometry.Coordinates[1]
cell, err := h3utils.GetCell(longitude, latitude, 15)
if err != nil {
- return GeocodeResult{}, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", longitude, latitude)
+ return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", longitude, latitude)
}
- country_s = strings.ToLower(feature.Properties.CountryA)
- return GeocodeResult{
+ country_s := strings.ToLower(feature.Properties.CountryA)
+ return &GeocodeResult{
Address: types.Address{
Country: country_s,
Locality: feature.Properties.Locality,
@@ -203,9 +217,11 @@ func Geocode(ctx context.Context, org *models.Organization, a types.Address) (Ge
Street: feature.Properties.Street,
Unit: "",
},
- Cell: cell,
- Longitude: feature.Geometry.Coordinates[0],
- Latitude: feature.Geometry.Coordinates[1],
+ Cell: cell,
+ Location: types.Location{
+ Longitude: feature.Geometry.Coordinates[0],
+ Latitude: feature.Geometry.Coordinates[1],
+ },
}, nil
}
@@ -239,7 +255,7 @@ func allFeaturesIdenticalEnough(features []stadia.GeocodeFeature) bool {
}
return true
}
-func maybeAddServiceArea(req *stadia.StructuredGeocodeRequest, org *models.Organization) {
+func maybeAddServiceArea(req stadia.RequestGeocode, org *models.Organization) {
if org.ServiceAreaXmax.IsNull() ||
org.ServiceAreaYmax.IsNull() ||
org.ServiceAreaXmin.IsNull() ||
@@ -250,10 +266,7 @@ func maybeAddServiceArea(req *stadia.StructuredGeocodeRequest, org *models.Organ
ymax := org.ServiceAreaYmax.MustGet()
xmin := org.ServiceAreaXmin.MustGet()
ymin := org.ServiceAreaYmin.MustGet()
- req.BoundaryRectMaxLon = &xmax
- req.BoundaryRectMaxLat = &ymax
- req.BoundaryRectMinLon = &xmin
- req.BoundaryRectMinLat = &ymin
+ req.SetBoundaryRect(xmin, ymin, xmax, ymax)
if org.ServiceAreaCentroidX.IsNull() || org.ServiceAreaCentroidY.IsNull() {
return
@@ -261,6 +274,5 @@ func maybeAddServiceArea(req *stadia.StructuredGeocodeRequest, org *models.Organ
centroid_x := org.ServiceAreaCentroidX.MustGet()
centroid_y := org.ServiceAreaCentroidY.MustGet()
- req.FocusPointLat = ¢roid_y
- req.FocusPointLng = ¢roid_x
+ req.SetFocusPoint(centroid_x, centroid_y)
}
diff --git a/platform/geom/geom.go b/platform/geom/geom.go
index 802b9a32..d14be3b2 100644
--- a/platform/geom/geom.go
+++ b/platform/geom/geom.go
@@ -2,8 +2,10 @@ package geom
import (
"fmt"
+
+ "github.com/Gleipnir-Technology/nidus-sync/platform/types"
)
-func PostgisPointQuery(longitude, latitude float64) string {
- return fmt.Sprintf("ST_SetSRID(ST_MakePoint(%f, %f), 4326)", longitude, latitude)
+func PostgisPointQuery(location types.Location) string {
+ return fmt.Sprintf("ST_SetSRID(ST_MakePoint(%f, %f), 4326)", location.Longitude, location.Latitude)
}
diff --git a/platform/lead.go b/platform/lead.go
new file mode 100644
index 00000000..b4f9536f
--- /dev/null
+++ b/platform/lead.go
@@ -0,0 +1,184 @@
+package platform
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/Gleipnir-Technology/bob"
+ "github.com/Gleipnir-Technology/bob/dialect/psql"
+ "github.com/Gleipnir-Technology/bob/dialect/psql/um"
+ "github.com/Gleipnir-Technology/nidus-sync/db"
+ "github.com/Gleipnir-Technology/nidus-sync/db/enums"
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+ "github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
+ "github.com/Gleipnir-Technology/nidus-sync/platform/geom"
+ "github.com/aarondl/opt/omit"
+ "github.com/aarondl/opt/omitnull"
+ "github.com/rs/zerolog/log"
+)
+
+// Create a lead from the given signal and site
+func LeadCreate(ctx context.Context, user User, signal_id int32, site_id int32, pool_location *Location) (*int32, error) {
+ txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
+ defer txn.Rollback(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("start transaction: %w", err)
+ }
+
+ lead, err := models.Leads.Insert(&models.LeadSetter{
+ Created: omit.From(time.Now()),
+ Creator: omit.From(int32(user.ID)),
+ // ID
+ OrganizationID: omit.From(int32(user.Organization.ID())),
+ SiteID: omitnull.From(site_id),
+ Type: omit.From(enums.LeadtypeGreenPool),
+ }).One(ctx, txn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create lead: %w", err)
+ }
+ _, err = psql.Update(
+ um.Table("signal"),
+ um.SetCol("addressed").ToArg(time.Now()),
+ um.SetCol("addressor").ToArg(user.ID),
+ um.Where(psql.Quote("id").EQ(psql.Arg(signal_id))),
+ ).Exec(ctx, txn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update signal %d: %w", signal_id, err)
+ }
+ if pool_location != nil {
+ log.Info().Float64("lat", pool_location.Latitude).Float64("lng", pool_location.Longitude).Msg("got pool location")
+ geom_query := geom.PostgisPointQuery(*pool_location)
+ _, err = psql.Update(
+ um.Table("pool"),
+ um.SetCol("geometry").To(geom_query),
+ um.From("signal_pool"),
+ um.Where(psql.Quote("signal_pool", "pool_id").EQ(psql.Quote("pool", "id"))),
+ um.Where(psql.Quote("signal_pool", "signal_id").EQ(psql.Arg(signal_id))),
+ ).Exec(ctx, txn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update pool through signal %d: %w", signal_id, err)
+ }
+ }
+ txn.Commit(ctx)
+ return &lead.ID, nil
+}
+
+// Create a lead from the given signal and site
+func LeadCreateFromPublicreport(ctx context.Context, user User, report_id string) (*int32, error) {
+ txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
+ defer txn.Rollback(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("start transaction: %w", err)
+ }
+
+ location, err := models.PublicreportReportLocations.Query(
+ models.SelectWhere.PublicreportReportLocations.PublicID.EQ(report_id),
+ models.SelectWhere.PublicreportReportLocations.OrganizationID.EQ(user.Organization.ID()),
+ ).One(ctx, txn)
+ if err != nil {
+ return nil, fmt.Errorf("query report existence: %w", err)
+ }
+
+ // At this point we have a report. We need to decide where to put it based on either the address or
+ // the location.
+ var site_id int32
+ if location.AddressID.IsValue() {
+ site, err := siteFromAddress(ctx, txn, user, location.AddressID.MustGet())
+ if err != nil {
+ return nil, fmt.Errorf("site from address: %w", err)
+ }
+ site_id = site.ID
+ } else if location.LocationLatitude.IsValue() && location.LocationLongitude.IsValue() {
+ site, err := siteFromLocation(ctx, txn, user, Location{
+ Latitude: location.LocationLatitude.MustGet(),
+ Longitude: location.LocationLongitude.MustGet(),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("site from address: %w", err)
+ }
+ site_id = site.ID
+
+ } else if location.AddressRaw.GetOr("") != "" {
+ // At this point we don't have an address, and we don't have GPS
+ // We'll try geocoding and creating an address from that.
+ site, err := siteFromAddressRaw(ctx, txn, user, location.AddressRaw.MustGet())
+ if err != nil {
+ return nil, fmt.Errorf("site from address: %w", err)
+ }
+ site_id = site.ID
+ } else {
+ // We have no structured address, no GPS, no unstructued address.
+ // There's really nothing we can make this lead from and have it be meaningful
+ return nil, errors.New("Refusing to create a lead with no location data.")
+ }
+
+ lead_type := enums.LeadtypeUnknown
+ tablename := location.TableName.MustGet()
+ switch tablename {
+ case "nuisance":
+ lead_type = enums.LeadtypePublicreportNuisance
+ case "water":
+ lead_type = enums.LeadtypePublicreportWater
+ }
+ lead, err := models.Leads.Insert(&models.LeadSetter{
+ Created: omit.From(time.Now()),
+ Creator: omit.From(int32(user.ID)),
+ // ID
+ OrganizationID: omit.From(int32(user.Organization.ID())),
+ SiteID: omitnull.From(site_id),
+ Type: omit.From(lead_type),
+ }).One(ctx, txn)
+ _, err = psql.Update(
+ um.Table("publicreport."+tablename),
+ um.SetCol("reviewed").ToArg(time.Now()),
+ um.SetCol("reviewer_id").ToArg(user.ID),
+ um.SetCol("status").ToArg(enums.PublicreportReportstatustypeReviewed),
+ um.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))),
+ ).Exec(ctx, txn)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update report %d: %w", report_id, err)
+ }
+ txn.Commit(ctx)
+
+ return &lead.ID, nil
+}
+func siteFromAddress(ctx context.Context, txn bob.Tx, user User, address_id int32) (*models.Site, error) {
+ site, err := models.Sites.Query(
+ models.SelectWhere.Sites.AddressID.EQ(address_id),
+ models.SelectWhere.Sites.OrganizationID.EQ(user.Organization.ID()),
+ ).One(ctx, txn)
+ if err == nil {
+ return site, nil
+ }
+ if err.Error() != "sql: no rows in result set" {
+ return nil, fmt.Errorf("query site: %w", err)
+ }
+ return SiteCreate(ctx, txn, user, address_id)
+}
+func siteFromAddressRaw(ctx context.Context, txn bob.Tx, user User, address string) (*models.Site, error) {
+ // Geocode
+ geo, err := geocode.GeocodeRaw(ctx, user.Organization.model, address)
+ if err != nil {
+ return nil, fmt.Errorf("geocode: %w", err)
+ }
+ a, err := geocode.EnsureAddress(ctx, txn, geo.Address, geo.Location)
+ if err != nil {
+ return nil, fmt.Errorf("ensure address: %w", err)
+ }
+ return siteFromAddress(ctx, txn, user, a.ID)
+}
+func siteFromLocation(ctx context.Context, txn bob.Tx, user User, location Location) (*models.Site, error) {
+ // Reverse geocode at the location
+ resp, err := geocode.ReverseGeocode(ctx, location)
+ if err != nil {
+ return nil, fmt.Errorf("reverse geocode: %w", err)
+ }
+ // Ensure we have an address at that newly created location
+ a, err := geocode.EnsureAddress(ctx, txn, resp.Address, resp.Location)
+ if err != nil {
+ return nil, fmt.Errorf("ensure address: %w", err)
+ }
+ return siteFromAddress(ctx, txn, user, a.ID)
+}
diff --git a/platform/publicreport.go b/platform/publicreport.go
new file mode 100644
index 00000000..4dd53b22
--- /dev/null
+++ b/platform/publicreport.go
@@ -0,0 +1,40 @@
+package platform
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ //"github.com/Gleipnir-Technology/bob"
+ "github.com/Gleipnir-Technology/bob/dialect/psql"
+ "github.com/Gleipnir-Technology/bob/dialect/psql/um"
+ "github.com/Gleipnir-Technology/nidus-sync/db"
+ "github.com/Gleipnir-Technology/nidus-sync/db/enums"
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+ "github.com/rs/zerolog/log"
+)
+
+func PublicreportInvalid(ctx context.Context, user User, report_id string) error {
+ location, err := models.PublicreportReportLocations.Query(
+ models.SelectWhere.PublicreportReportLocations.PublicID.EQ(report_id),
+ models.SelectWhere.PublicreportReportLocations.OrganizationID.EQ(user.Organization.ID()),
+ ).One(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return fmt.Errorf("query report existence: %w", err)
+ }
+
+ tablename := location.TableName.MustGet()
+ _, err = psql.Update(
+ um.Table("publicreport."+tablename),
+ um.SetCol("reviewed").ToArg(time.Now()),
+ um.SetCol("reviewer_id").ToArg(user.ID),
+ um.SetCol("status").ToArg(enums.PublicreportReportstatustypeInvalidated),
+ um.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))),
+ ).Exec(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ return fmt.Errorf("update report %s.%s: %w", tablename, report_id, err)
+ }
+
+ log.Info().Str("report-id", report_id).Str("tablename", tablename).Msg("Marked as invalid")
+ return nil
+}
diff --git a/platform/publicreport/nuisance.go b/platform/publicreport/nuisance.go
index bbb92d92..1130068b 100644
--- a/platform/publicreport/nuisance.go
+++ b/platform/publicreport/nuisance.go
@@ -79,6 +79,7 @@ func NuisanceReportForOrganization(ctx context.Context, org_id int32) ([]Nuisanc
),
sm.From("publicreport.nuisance"),
sm.Where(psql.Quote("publicreport", "nuisance", "organization_id").EQ(psql.Arg(org_id))),
+ sm.Where(psql.Quote("publicreport", "nuisance", "reviewed").IsNull()),
), scan.StructMapper[Nuisance]())
if err != nil {
return nil, fmt.Errorf("get reports: %w", err)
diff --git a/platform/publicreport/water.go b/platform/publicreport/water.go
index c01a5a14..40007245 100644
--- a/platform/publicreport/water.go
+++ b/platform/publicreport/water.go
@@ -83,6 +83,7 @@ func WaterReportForOrganization(ctx context.Context, org_id int32) ([]Water, err
),
sm.From("publicreport.water"),
sm.Where(psql.Quote("publicreport", "water", "organization_id").EQ(psql.Arg(org_id))),
+ sm.Where(psql.Quote("publicreport", "water", "reviewed").IsNull()),
), scan.StructMapper[Water]())
if err != nil {
return nil, fmt.Errorf("get reports: %w", err)
diff --git a/platform/site.go b/platform/site.go
new file mode 100644
index 00000000..20c863b2
--- /dev/null
+++ b/platform/site.go
@@ -0,0 +1,64 @@
+package platform
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/Gleipnir-Technology/bob"
+ "github.com/Gleipnir-Technology/bob/dialect/psql"
+ "github.com/Gleipnir-Technology/bob/dialect/psql/sm"
+ "github.com/Gleipnir-Technology/bob/types/pgtypes"
+ "github.com/Gleipnir-Technology/nidus-sync/db"
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+ nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
+ "github.com/aarondl/opt/omit"
+ "github.com/aarondl/opt/omitnull"
+ "github.com/stephenafamo/scan"
+)
+
+func SiteFromSignal(ctx context.Context, user User, signal_id int32) (*int32, error) {
+ type _Row struct {
+ ID int32 `db:"site_id"`
+ }
+ site, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
+ sm.Columns(
+ "pool.site_id AS site_id",
+ ),
+ sm.From("signal_pool"),
+ sm.InnerJoin("pool").OnEQ(
+ psql.Quote("signal_pool", "pool_id"),
+ psql.Quote("pool", "id"),
+ ),
+ sm.InnerJoin("site").On(
+ psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")),
+ ),
+ sm.Where(psql.Quote("signal_pool", "signal_id").EQ(psql.Arg(signal_id))),
+ sm.Where(psql.Quote("site", "organization_id").EQ(psql.Arg(user.Organization.ID()))),
+ ), scan.StructMapper[_Row]())
+ if err != nil {
+ if err.Error() == "sql: no rows in result set" {
+ return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Can't make a lead from signal %d: %w", signal_id, err)
+ }
+ return nil, fmt.Errorf("failed getting site: %w", err)
+ }
+ return &site.ID, nil
+}
+func SiteCreate(ctx context.Context, txn bob.Tx, user User, address_id int32) (*models.Site, error) {
+ return models.Sites.Insert(&models.SiteSetter{
+ AddressID: omit.From(address_id),
+ Created: omit.From(time.Now()),
+ CreatorID: omit.From(int32(user.ID)),
+ FileID: omitnull.FromPtr[int32](nil),
+ //ID:
+ Notes: omit.From(""),
+ OrganizationID: omit.From(user.Organization.ID()),
+ OwnerName: omit.From(""),
+ OwnerPhoneE164: omitnull.FromPtr[string](nil),
+ ParcelID: omitnull.FromPtr[int32](nil),
+ ResidentOwned: omitnull.FromPtr[bool](nil),
+ Tags: omit.From(pgtypes.HStore{}),
+ Version: omit.From(int32(1)),
+ }).One(ctx, txn)
+}
diff --git a/platform/type.go b/platform/type.go
index e7161a88..1a9495ad 100644
--- a/platform/type.go
+++ b/platform/type.go
@@ -4,8 +4,11 @@ import (
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
+ "github.com/Gleipnir-Technology/nidus-sync/platform/types"
)
+type Location = types.Location
+
type ClientSync struct {
Fieldseeker FieldseekerRecordsSync
Since time.Time
diff --git a/platform/types/location.go b/platform/types/location.go
index 0e35045f..d1cd4efb 100644
--- a/platform/types/location.go
+++ b/platform/types/location.go
@@ -1,6 +1,14 @@
package types
+import (
+ "fmt"
+)
+
type Location struct {
Latitude float64 `db:"latitude" json:"latitude"`
Longitude float64 `db:"longitude" json:"longitude"`
}
+
+func (l Location) String() string {
+ return fmt.Sprintf("%f %f", l.Longitude, l.Latitude)
+}
diff --git a/stadia/geocode_raw.go b/stadia/geocode_raw.go
new file mode 100644
index 00000000..821310bf
--- /dev/null
+++ b/stadia/geocode_raw.go
@@ -0,0 +1,69 @@
+package stadia
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/google/go-querystring/query"
+)
+
+type RequestGeocodeRaw struct {
+ Text string `url:"text" json:"text"`
+
+ // Boundary circle parameters
+ BoundaryCircleLat *float64 `url:"boundary.circle.lat,omitempty"`
+ BoundaryCircleLon *float64 `url:"boundary.circle.lon,omitempty"`
+ BoundaryCircleRadius *float64 `url:"boundary.circle.radius,omitempty"`
+
+ // Boundary parameters
+ BoundaryRectMaxLat *float64 `url:"boundary.rect.max_lat,omitempty"`
+ BoundaryRectMinLat *float64 `url:"boundary.rect.min_lat,omitempty"`
+ BoundaryRectMaxLon *float64 `url:"boundary.rect.max_lon,omitempty"`
+ BoundaryRectMinLon *float64 `url:"boundary.rect.min_lon,omitempty"`
+
+ // Focus point
+ FocusPointLat *float64 `url:"focus.point.lat,omitempty" json:",omitempty"`
+ FocusPointLng *float64 `url:"focus.point.lon,omitempty" json:",omitempty"`
+
+ // Other parameters
+ Lang *string `url:"lang,omitempty" json:"lang,omitempty"`
+ Layers []string `url:"layers,omitempty,comma" json:"layers,omitempty"`
+ Sources []string `url:"sources,omitempty,comma" json:"sources,omitempty"`
+ Size *int `url:"size,omitempty" json:"size,omitempty"`
+}
+
+func (r *RequestGeocodeRaw) SetBoundaryRect(xmin, ymin, xmax, ymax float64) {
+ r.BoundaryRectMaxLat = &ymax
+ r.BoundaryRectMinLat = &ymin
+ r.BoundaryRectMaxLon = &xmax
+ r.BoundaryRectMinLon = &xmin
+}
+func (r *RequestGeocodeRaw) SetFocusPoint(x, y float64) {
+ r.FocusPointLat = &y
+ r.FocusPointLng = &x
+}
+func (s *StadiaMaps) GeocodeRaw(ctx context.Context, req RequestGeocodeRaw) (*GeocodeResponse, error) {
+ // https://docs.stadiamaps.com/geocoding-search-autocomplete/search/
+ var result GeocodeResponse
+
+ query, err := query.Values(req)
+ if err != nil {
+ return nil, fmt.Errorf("structured geocode query: %w", err)
+ }
+ //var api_error Error
+ resp, err := s.client.R().
+ SetQueryParamsFromValues(query).
+ SetContext(ctx).
+ SetResult(&result).
+ SetPathParam("urlBase", s.urlBase).
+ SetQueryParam("api_key", s.APIKey).
+ Get("https://{urlBase}/geocoding/v1/search")
+ if err != nil {
+ return nil, fmt.Errorf("geocoding get: %w", err)
+ }
+
+ if !resp.IsSuccess() {
+ return nil, parseError(resp)
+ }
+ return &result, nil
+}
diff --git a/stadia/structured_geocode.go b/stadia/geocode_structured.go
similarity index 81%
rename from stadia/structured_geocode.go
rename to stadia/geocode_structured.go
index 91d41227..7aa008dd 100644
--- a/stadia/structured_geocode.go
+++ b/stadia/geocode_structured.go
@@ -7,8 +7,8 @@ import (
"github.com/google/go-querystring/query"
)
-// StructuredGeocodeRequest represents the query parameters for structured geocoding
-type StructuredGeocodeRequest struct {
+// RequestGeocodeStructured represents the query parameters for structured geocoding
+type RequestGeocodeStructured struct {
// Address components
Address *string `url:"address,omitempty" json:"address,omitempty"`
Neighbourhood *string `url:"neighbourhood,omitempty" json:"neighbourhood,omitempty"`
@@ -24,7 +24,7 @@ type StructuredGeocodeRequest struct {
BoundaryCircleLon *float64 `url:"boundary.circle.lon,omitempty"`
BoundaryCircleRadius *float64 `url:"boundary.circle.radius,omitempty"`
- BoundaryCountry []string `url:"boundary.country,omitempty,comma" json:"boundary.country,omitempty,comma"`
+ BoundaryCountry []string `url:"boundary.country,omitempty,comma" json:"boundary.country,omitempty"`
BoundaryGid *string `url:"boundary.gid,omitempty" json:"boundary.gid,omitempty"`
// Boundary parameters
@@ -38,13 +38,24 @@ type StructuredGeocodeRequest struct {
FocusPointLng *float64 `url:"focus.point.lon,omitempty" json:",omitempty"`
// Other parameters
- Layers []string `url:"layers,omitempty,comma" json:"layers,omitempty,comma"`
- Sources []string `url:"sources,omitempty,comma" json:"sources,omitempty,comma"`
+ Layers []string `url:"layers,omitempty,comma" json:"layers,omitempty"`
+ Sources []string `url:"sources,omitempty,comma" json:"sources,omitempty"`
Size *int `url:"size,omitempty" json:"size,omitempty"`
Lang *string `url:"lang,omitempty" json:"lang,omitempty"`
}
-func (s *StadiaMaps) StructuredGeocode(ctx context.Context, req StructuredGeocodeRequest) (*GeocodeResponse, error) {
+func (r *RequestGeocodeStructured) SetBoundaryRect(xmin, ymin, xmax, ymax float64) {
+ r.BoundaryRectMaxLat = &ymax
+ r.BoundaryRectMinLat = &ymin
+ r.BoundaryRectMaxLon = &xmax
+ r.BoundaryRectMinLon = &xmin
+}
+func (r *RequestGeocodeStructured) SetFocusPoint(x, y float64) {
+ r.FocusPointLat = &y
+ r.FocusPointLng = &x
+}
+
+func (s *StadiaMaps) GeocodeStructured(ctx context.Context, req RequestGeocodeStructured) (*GeocodeResponse, error) {
// https://docs.stadiamaps.com/geocoding-search-autocomplete/structured-search/
// curl "https://api.stadiamaps.com/geocoding/v1/search/structured?address=P%C3%B5hja%20pst%2027a®ion=Harju&country=EE&api_key=YOUR-API-KEY"
var result GeocodeResponse
@@ -70,7 +81,3 @@ func (s *StadiaMaps) StructuredGeocode(ctx context.Context, req StructuredGeocod
}
return &result, nil
}
-
-func (sgr StructuredGeocodeRequest) endpoint() string {
- return "/v1/search/structured"
-}
diff --git a/stadia/request.go b/stadia/request.go
new file mode 100644
index 00000000..b9f8a8ca
--- /dev/null
+++ b/stadia/request.go
@@ -0,0 +1,6 @@
+package stadia
+
+type RequestGeocode interface {
+ SetBoundaryRect(xmin, ymin, xmax, ymax float64)
+ SetFocusPoint(x, y float64)
+}
diff --git a/stadia/reverse_geocode.go b/stadia/reverse_geocode.go
new file mode 100644
index 00000000..c611d3ce
--- /dev/null
+++ b/stadia/reverse_geocode.go
@@ -0,0 +1,49 @@
+package stadia
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/google/go-querystring/query"
+)
+
+type RequestReverseGeocode struct {
+ Latitude float64 `url:"point.lat" json:"point.lat"`
+ Longitude float64 `url:"point.lon" json:"point.lon"`
+
+ // Boundary circle parameters
+ BoundaryCircleRadius *float64 `url:"boundary.circle.radius,omitempty"`
+ BoundaryCountry []string `url:"boundary.country,omitempty"`
+ BoundaryGID string `url:"boundary.gid,omitempty"`
+
+ // Other parameters
+ Layers []string `url:"layers,omitempty,comma" json:"layers,omitempty"`
+ Size *int `url:"size,omitempty" json:"size,omitempty"`
+ Sources []string `url:"sources,omitempty,comma" json:"sources,omitempty"`
+}
+
+func (s *StadiaMaps) ReverseGeocode(ctx context.Context, req RequestReverseGeocode) (*GeocodeResponse, error) {
+ // https://docs.stadiamaps.com/geocoding-search-autocomplete/reverse-search/
+ var result GeocodeResponse
+
+ query, err := query.Values(req)
+ if err != nil {
+ return nil, fmt.Errorf("reverse geocode query: %w", err)
+ }
+ //var api_error Error
+ resp, err := s.client.R().
+ SetQueryParamsFromValues(query).
+ SetContext(ctx).
+ SetResult(&result).
+ SetPathParam("urlBase", s.urlBase).
+ SetQueryParam("api_key", s.APIKey).
+ Get("https://{urlBase}/geocoding/v2/reverse")
+ if err != nil {
+ return nil, fmt.Errorf("reverse geocoding get: %w", err)
+ }
+
+ if !resp.IsSuccess() {
+ return nil, parseError(resp)
+ }
+ return &result, nil
+}