diff --git a/api/routes.go b/api/routes.go index fbc7ff59..6b202d19 100644 --- a/api/routes.go +++ b/api/routes.go @@ -60,6 +60,7 @@ func AddRoutes(r *mux.Router) { r.Handle("/signal", authenticatedHandlerJSON(signal.List)).Methods("GET") site := resource.Site(router) r.Handle("/site", authenticatedHandlerJSONSlice(site.List)).Methods("GET") + r.Handle("/site/{id}", authenticatedHandlerJSON(site.ByIDGet)).Methods("GET").Name("site.ByIDGet") sync := resource.Sync(r) r.Handle("/sync", authenticatedHandlerJSONSlice(sync.List)).Methods("GET") r.Handle("/sudo/email", authenticatedHandlerJSONPost(postSudoEmail)).Methods("POST") diff --git a/platform/compliance.go b/platform/compliance.go index bc194628..4e5a88de 100644 --- a/platform/compliance.go +++ b/platform/compliance.go @@ -3,6 +3,7 @@ package platform import ( "context" "fmt" + "strconv" "time" "github.com/Gleipnir-Technology/bob/dialect/psql" @@ -10,6 +11,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/platform/background" + "github.com/Gleipnir-Technology/nidus-sync/platform/event" "github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" @@ -90,6 +92,8 @@ func ComplianceRequestMailerCreate(ctx context.Context, user User, site_id int32 if err != nil { return 0, fmt.Errorf("create background compliance mailer job: %w", err) } + event.Updated(event.TypeSite, user.Organization.ID, strconv.Itoa(int(site.ID))) txn.Commit(ctx) + return req.ID, nil } diff --git a/platform/event/event.go b/platform/event/event.go index 08992964..30dcc432 100644 --- a/platform/event/event.go +++ b/platform/event/event.go @@ -94,6 +94,7 @@ const ( TypeRMOWater TypeSession TypeSignal + TypeSite ) func Created(t ResourceType, organization_id int32, uri_id string) { @@ -154,6 +155,8 @@ func resourceString(t ResourceType) string { return "sync:session" case TypeSignal: return "sync:signal" + case TypeSite: + return "sync:site" default: return "unknown" } @@ -180,6 +183,8 @@ func makeURI(t ResourceType, id string) string { return config.MakeURLReport("/api/session") case TypeSignal: return config.MakeURLReport("/api/signal/%s", id) + case TypeSite: + return config.MakeURLReport("/api/site/%s", id) default: return config.MakeURLReport("/unknown") } diff --git a/platform/site.go b/platform/site.go index 28aea195..979595ff 100644 --- a/platform/site.go +++ b/platform/site.go @@ -8,6 +8,7 @@ import ( "github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/Gleipnir-Technology/nidus-sync/db" @@ -47,6 +48,18 @@ func SiteFromSignal(ctx context.Context, user User, signal_id int32) (*int32, er } return &site.ID, nil } +func SiteByID(ctx context.Context, user User, id int32) (*types.Site, error) { + query := siteQuery() + query.Apply( + sm.Where(models.Sites.Columns.ID.EQ(psql.Arg(id))), + sm.Where(models.Sites.Columns.OrganizationID.EQ(psql.Arg(user.Organization.ID))), + ) + sites, err := siteQueryToRows(ctx, query) + if err != nil { + return nil, err + } + return sites[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), @@ -65,71 +78,12 @@ func SiteCreate(ctx context.Context, txn bob.Tx, user User, address_id int32) (* }).One(ctx, txn) } func SiteList(ctx context.Context, user User, limit int) ([]*types.Site, error) { - rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( - sm.Columns( - "address.country AS \"address.country\"", - "address.locality AS \"address.locality\"", - "COALESCE(address.location_latitude, 0) AS \"address.location.latitude\"", - "COALESCE(address.location_longitude, 0) AS \"address.location.longitude\"", - "address.number_ AS \"address.number\"", - "address.postal_code AS \"address.postal_code\"", - "address.region AS \"address.region\"", - "address.street AS \"address.street\"", - "address.unit AS \"address.unit\"", - "site.created AS \"created\"", - "site.id AS \"id\"", - "site.notes AS \"notes\"", - "site.owner_name AS \"owner.name\"", - "site.owner_phone_e164 AS \"owner.phone\"", - "COALESCE(site.parcel_id, 0) AS \"parcel.id\"", - "COALESCE(parcel.apn, '') AS \"parcel.apn\"", - "COALESCE(parcel.description, '') AS \"parcel.description\"", - ), - sm.From("site"), - sm.InnerJoin("address").OnEQ( - psql.Quote("site", "address_id"), - psql.Quote("address", "id"), - ), - sm.LeftJoin("parcel").OnEQ( - psql.Quote("site", "parcel_id"), - psql.Quote("parcel", "id"), - ), + query := siteQuery() + query.Apply( sm.Where(psql.Quote("site", "organization_id").EQ(psql.Arg(user.Organization.ID))), sm.Limit(limit), - ), scan.StructMapper[types.Site]()) - if err != nil { - return nil, fmt.Errorf("query sites: %w", err) - } - site_ids := make([]int32, len(rows)) - results := make([]*types.Site, len(rows)) - for i, row := range rows { - results[i] = &row - site_ids[i] = row.ID - } - features_by_site_id, err := featuresBySiteID(ctx, site_ids) - if err != nil { - return nil, fmt.Errorf("query features for sites: %w", err) - } - for _, result := range results { - features, ok := features_by_site_id[result.ID] - if !ok { - return nil, fmt.Errorf("impossible") - } - result.Features = features - } - leads_by_site_id, err := leadsBySiteID(ctx, site_ids) - if err != nil { - return nil, fmt.Errorf("query leads for sites: %w", err) - } - for _, result := range results { - leads, ok := leads_by_site_id[result.ID] - if !ok { - return nil, fmt.Errorf("impossible") - } - result.Leads = leads - } - - return results, nil + ) + return siteQueryToRows(ctx, query) } func SitesByID(ctx context.Context, ids []int32) (map[int32]*models.Site, error) { rows, err := models.Sites.Query( @@ -184,3 +138,71 @@ func siteFromLocation(ctx context.Context, txn bob.Tx, user User, location types } return siteFromAddress(ctx, txn, user, a.ID) } +func siteQuery() bob.BaseQuery[*dialect.SelectQuery] { + return psql.Select( + sm.Columns( + "address.country AS \"address.country\"", + "address.locality AS \"address.locality\"", + "COALESCE(address.location_latitude, 0) AS \"address.location.latitude\"", + "COALESCE(address.location_longitude, 0) AS \"address.location.longitude\"", + "address.number_ AS \"address.number\"", + "address.postal_code AS \"address.postal_code\"", + "address.region AS \"address.region\"", + "address.street AS \"address.street\"", + "address.unit AS \"address.unit\"", + "site.created AS \"created\"", + "site.id AS \"id\"", + "site.notes AS \"notes\"", + "site.owner_name AS \"owner.name\"", + "site.owner_phone_e164 AS \"owner.phone\"", + "COALESCE(site.parcel_id, 0) AS \"parcel.id\"", + "COALESCE(parcel.apn, '') AS \"parcel.apn\"", + "COALESCE(parcel.description, '') AS \"parcel.description\"", + ), + sm.From("site"), + sm.InnerJoin("address").OnEQ( + psql.Quote("site", "address_id"), + psql.Quote("address", "id"), + ), + sm.LeftJoin("parcel").OnEQ( + psql.Quote("site", "parcel_id"), + psql.Quote("parcel", "id"), + ), + ) +} +func siteQueryToRows(ctx context.Context, query bob.BaseQuery[*dialect.SelectQuery]) ([]*types.Site, error) { + rows, err := bob.All(ctx, db.PGInstance.BobDB, query, scan.StructMapper[types.Site]()) + if err != nil { + return nil, fmt.Errorf("query sites: %w", err) + } + site_ids := make([]int32, len(rows)) + results := make([]*types.Site, len(rows)) + for i, row := range rows { + results[i] = &row + site_ids[i] = row.ID + } + features_by_site_id, err := featuresBySiteID(ctx, site_ids) + if err != nil { + return nil, fmt.Errorf("query features for sites: %w", err) + } + for _, result := range results { + features, ok := features_by_site_id[result.ID] + if !ok { + return nil, fmt.Errorf("impossible") + } + result.Features = features + } + leads_by_site_id, err := leadsBySiteID(ctx, site_ids) + if err != nil { + return nil, fmt.Errorf("query leads for sites: %w", err) + } + for _, result := range results { + leads, ok := leads_by_site_id[result.ID] + if !ok { + return nil, fmt.Errorf("impossible") + } + result.Leads = leads + } + + return results, nil +} diff --git a/platform/types/site.go b/platform/types/site.go index 1759272c..96b8329e 100644 --- a/platform/types/site.go +++ b/platform/types/site.go @@ -22,6 +22,7 @@ type Site struct { ResidentOwned *bool `db:"resident_owned" json:"resident_owned"` Tags map[string]string `db:"tags" json:"tags"` Version int32 `db:"version" json:"version"` + URI string `db:"-" json:"uri"` } func SiteFromModel(s *models.Site) Site { diff --git a/resource/site.go b/resource/site.go index 6b9b7921..ccd99edc 100644 --- a/resource/site.go +++ b/resource/site.go @@ -3,12 +3,13 @@ package resource import ( "context" "net/http" + "strconv" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/aarondl/opt/null" - //"github.com/gorilla/mux" + "github.com/gorilla/mux" ) type siteR struct { @@ -21,6 +22,19 @@ func Site(r *router) *siteR { } } +func (res *siteR) ByIDGet(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*types.Site, *nhttp.ErrorWithStatus) { + vars := mux.Vars(r) + id_str := vars["id"] + id, err := strconv.Atoi(id_str) + if err != nil { + return nil, nhttp.NewBadRequest("'%s' is not a valid site ID: %w", id_str, err) + } + site, err := platform.SiteByID(ctx, user, int32(id)) + if err != nil { + return nil, nhttp.NewError("site by id: %w", err) + } + return site, nil +} func (res *siteR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]*types.Site, *nhttp.ErrorWithStatus) { limit := 1000 if query.Limit != nil { @@ -30,5 +44,12 @@ func (res *siteR) List(ctx context.Context, r *http.Request, user platform.User, if err != nil { return nil, nhttp.NewError("list signals: %w", err) } + for _, site := range sites { + uri, err := res.router.IDToURI("site.ByIDGet", int(site.ID)) + if err != nil { + return nil, nhttp.NewError("set uri: %w", err) + } + site.URI = uri + } return sites, nil } diff --git a/ts/components/ReviewSiteColumnAction.vue b/ts/components/ReviewSiteColumnAction.vue index b6d0366f..66d7cd0a 100644 --- a/ts/components/ReviewSiteColumnAction.vue +++ b/ts/components/ReviewSiteColumnAction.vue @@ -4,23 +4,19 @@
select a site to see actions
- + :disabled="!selectedSite" + icon="bi-check-circle" + :loading="submitting" + text="Send Compliance Mailer" + variant="success" + />