From 725945d95c3db4d905a02ea8f5ef9b05c00515b9 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 15 May 2026 20:10:14 +0000 Subject: [PATCH] WIP: creating contact resource --- api/handler.go | 2 +- api/routes.go | 3 + db/query/comms/contact.go | 9 +-- db/query/comms/contact_email.go | 52 +++++++++++++++ db/query/comms/contact_phone.go | 28 ++++++--- db/query/public/mailer.go | 70 +++++++++++++++++++++ platform/contact.go | 54 ++++++++++++++++ platform/mailer.go | 37 +---------- platform/publicreport/report.go | 14 ++--- platform/types/contact.go | 51 +++++++-------- platform/types/publicreport.go | 2 +- platform/types/site.go | 7 ++- resource/contact.go | 48 ++++++++++++++ ts/components/CardPublicReport.vue | 12 ++-- ts/components/CommunicationColumnAction.vue | 8 +-- ts/components/PublicReportCardWater.vue | 4 +- ts/rmo/content/compliance/Complete.vue | 6 +- ts/rmo/content/compliance/Contact.vue | 4 +- ts/rmo/content/compliance/Submit.vue | 4 +- ts/store/resource.ts | 32 +++++++--- ts/type/api.ts | 61 +++++++++++------- ts/view/review/Contact.vue | 8 +++ 22 files changed, 381 insertions(+), 135 deletions(-) create mode 100644 db/query/comms/contact_email.go create mode 100644 db/query/public/mailer.go create mode 100644 platform/contact.go create mode 100644 resource/contact.go diff --git a/api/handler.go b/api/handler.go index a5cc95c3..9c6315a5 100644 --- a/api/handler.go +++ b/api/handler.go @@ -30,7 +30,7 @@ type handlerFunctionDelete func(context.Context, *http.Request, platform.User) * type handlerFunctionGet[T any] func(context.Context, *http.Request, resource.QueryParams) (*T, *nhttp.ErrorWithStatus) type handlerFunctionGetAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (T, *nhttp.ErrorWithStatus) type handlerFunctionGetImage func(context.Context, *http.Request, platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus) -type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus) +type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, resource.QueryParams) ([]T, *nhttp.ErrorWithStatus) type handlerFunctionGetSliceAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) ([]T, *nhttp.ErrorWithStatus) type handlerFunctionPost[RequestType any, ResponseType any] func(context.Context, *http.Request, RequestType) (ResponseType, *nhttp.ErrorWithStatus) type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus) diff --git a/api/routes.go b/api/routes.go index 8d736037..88386083 100644 --- a/api/routes.go +++ b/api/routes.go @@ -48,6 +48,7 @@ func AddRoutesRMO(r *mux.Router) { func AddRoutesSync(r *mux.Router) { router := resource.NewRouter(r) + contact := resource.Contact(router) compliance_request := resource.ComplianceRequest(router) district := resource.District(router) geocode := resource.Geocode(router) @@ -107,6 +108,8 @@ func AddRoutesSync(r *mux.Router) { r.Handle("/compliance-request/mailer", authenticatedHandlerJSONPost(compliance_request.CreateMailer)).Methods("POST") //r.HandleFunc("/compliance-request/image/pool/{public_id}", getComplianceRequestImagePool).Methods("GET") r.Handle("/configuration/integration/arcgis", authenticatedHandlerJSONPost(postConfigurationIntegrationArcgis)).Methods("POST") + r.Handle("/contact", authenticatedHandlerJSONSlice(contact.List)).Methods("GET") + r.Handle("/contact/{id}", authenticatedHandlerJSON(contact.ByIDGet)).Methods("GET").Name("contact.ByIDGet") email := resource.Email(router) r.Handle("/email/{id}", authenticatedHandlerJSON(email.Get)).Methods("GET").Name("email.ByIDGet") r.Handle("/events", auth.NewEnsureAuth(streamEvents)).Methods("GET") diff --git a/db/query/comms/contact.go b/db/query/comms/contact.go index 8b6bf80f..35c852ff 100644 --- a/db/query/comms/contact.go +++ b/db/query/comms/contact.go @@ -35,13 +35,10 @@ func ContactUpdateName(ctx context.Context, txn db.Ex, id int64, name string) er return db.ExecuteNoneTx(ctx, txn, statement) } -/* -func ContactsFromAddress(ctx context.Context, address string) ([]model.Contact, error) { +func ContactsFromOrganizationID(ctx context.Context, txn db.Ex, org_id int64) ([]model.Contact, error) { statement := table.Contact.SELECT( table.Contact.AllColumns, ).FROM(table.Contact). - WHERE(table.Contact.Source.EQ(postgres.String(address)).OR( - table.Contact.Destination.EQ(postgres.String(address)))) - return db.ExecuteMany[model.Contact](ctx, statement) + WHERE(table.Contact.OrganizationID.EQ(postgres.Int(org_id))) + return db.ExecuteManyTx[model.Contact](ctx, txn, statement) } -*/ diff --git a/db/query/comms/contact_email.go b/db/query/comms/contact_email.go new file mode 100644 index 00000000..b7d3598f --- /dev/null +++ b/db/query/comms/contact_email.go @@ -0,0 +1,52 @@ +package comms + +import ( + "context" + "fmt" + + //"github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/nidus-sync/db" + //"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/enum" + "github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/table" +) + +func ContactEmailInsert(ctx context.Context, txn db.Ex, m model.ContactEmail) (model.ContactEmail, error) { + statement := table.ContactEmail.INSERT(table.ContactEmail.MutableColumns). + MODEL(m). + RETURNING(table.ContactEmail.AllColumns) + return db.ExecuteOneTx[model.ContactEmail](ctx, txn, statement) +} + +func ContactEmailFromAddress(ctx context.Context, txn db.Ex, address string) (model.ContactEmail, error) { + statement := table.ContactEmail.SELECT( + table.ContactEmail.AllColumns, + ).FROM(table.ContactEmail). + WHERE(table.ContactEmail.Address.EQ(postgres.String(address))) + return db.ExecuteOneTx[model.ContactEmail](ctx, txn, statement) +} +func ContactEmailByContactIDs(ctx context.Context, txn db.Ex, contact_ids []int64) (result map[int64][]model.ContactEmail, err error) { + sql_ids := make([]postgres.Expression, len(contact_ids)) + for i, contact_id := range contact_ids { + sql_ids[i] = postgres.Int(contact_id) + } + statement := table.ContactEmail.SELECT( + table.ContactEmail.AllColumns, + ).FROM(table.ContactEmail). + WHERE(table.ContactEmail.ContactID.IN(sql_ids...)) + rows, err := db.ExecuteManyTx[model.ContactEmail](ctx, txn, statement) + if err != nil { + return result, fmt.Errorf("query by contact IDs: %w", err) + } + for _, contact_id := range contact_ids { + result[contact_id] = make([]model.ContactEmail, 0) + } + for _, row := range rows { + id := int64(row.ContactID) + cur := result[id] + cur = append(cur, row) + result[id] = cur + } + return result, nil +} diff --git a/db/query/comms/contact_phone.go b/db/query/comms/contact_phone.go index 001f2ba5..a5583862 100644 --- a/db/query/comms/contact_phone.go +++ b/db/query/comms/contact_phone.go @@ -2,6 +2,7 @@ package comms import ( "context" + "fmt" //"github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/nidus-sync/db" @@ -46,14 +47,27 @@ func ContactPhoneUpdateStopMessageID(ctx context.Context, txn db.Ex, e164 string WHERE(table.ContactPhone.E164.EQ(postgres.String(e164))) return db.ExecuteNoneTx(ctx, txn, statement) } - -/* -func ContactPhonesFromAddress(ctx context.Context, address string) ([]model.ContactPhone, error) { +func ContactPhoneByContactIDs(ctx context.Context, txn db.Ex, contact_ids []int64) (result map[int64][]model.ContactPhone, err error) { + sql_ids := make([]postgres.Expression, len(contact_ids)) + for i, contact_id := range contact_ids { + sql_ids[i] = postgres.Int(contact_id) + } statement := table.ContactPhone.SELECT( table.ContactPhone.AllColumns, ).FROM(table.ContactPhone). - WHERE(table.ContactPhone.Source.EQ(postgres.String(address)).OR( - table.ContactPhone.Destination.EQ(postgres.String(address)))) - return db.ExecuteMany[model.ContactPhone](ctx, statement) + WHERE(table.ContactPhone.ContactID.IN(sql_ids...)) + rows, err := db.ExecuteManyTx[model.ContactPhone](ctx, txn, statement) + if err != nil { + return result, fmt.Errorf("query by contact IDs: %w", err) + } + for _, contact_id := range contact_ids { + result[contact_id] = make([]model.ContactPhone, 0) + } + for _, row := range rows { + id := int64(row.ContactID) + cur := result[id] + cur = append(cur, row) + result[id] = cur + } + return result, nil } -*/ diff --git a/db/query/public/mailer.go b/db/query/public/mailer.go new file mode 100644 index 00000000..1d43bb12 --- /dev/null +++ b/db/query/public/mailer.go @@ -0,0 +1,70 @@ +package public + +import ( + "context" + "errors" + "fmt" + + "github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" +) + +func mailerBaseQuery() postgres.SelectStatement { + return table.ComplianceReportRequest.SELECT( + table.Address.AllColumns, + table.ComplianceReportRequest.AllColumns, + ).FROM( + table.ComplianceReportRequest, + ).FROM( + table.ComplianceReportRequest.INNER_JOIN( + table.ComplianceReportRequestMailer, + table.ComplianceReportRequestMailer.ComplianceReportRequestID.EQ( + table.ComplianceReportRequest.ID), + ), + ).FROM( + table.ComplianceReportRequest.INNER_JOIN( + table.Lead, + table.Lead.ID.EQ( + table.ComplianceReportRequest.LeadID, + ), + ), + ).FROM( + table.Lead.INNER_JOIN( + table.Site, + table.Site.ID.EQ( + table.Lead.SiteID, + ), + ), + ).FROM( + table.Site.INNER_JOIN( + table.Address, + table.Address.ID.EQ( + table.Site.AddressID, + ), + ), + ) +} +func MailerFromPublicID(ctx context.Context, txn db.Ex, org_id int64, id int64) (*types.Mailer, error) { + statement := mailerBaseQuery().WHERE( + table.ComplianceReportRequest.ID.EQ(postgres.Int(id)).AND( + table.Site.OrganizationID.EQ(postgres.Int(org_id))), + ) + row, err := db.ExecuteOneTx[types.Mailer](ctx, txn, statement) + if err != nil { + if errors.Is(err, db.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("query: %w", err) + } + return &row, nil +} +func MailersFromOrganizationID(ctx context.Context, txn db.Ex, org_id int64, limit int64) ([]types.Mailer, error) { + statement := mailerBaseQuery().WHERE( + table.Site.OrganizationID.EQ(postgres.Int(org_id)), + ).ORDER_BY( + table.ComplianceReportRequest.Created, + ).LIMIT(limit) + return db.ExecuteManyTx[types.Mailer](ctx, txn, statement) +} diff --git a/platform/contact.go b/platform/contact.go new file mode 100644 index 00000000..d8fdcac7 --- /dev/null +++ b/platform/contact.go @@ -0,0 +1,54 @@ +package platform + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/nidus-sync/db" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" +) + +func ContactsForOrganization(ctx context.Context, org_id int32) (results []types.Contact, err error) { + txn := db.PGInstance.PGXPool + rows, err := querycomms.ContactsFromOrganizationID(ctx, txn, int64(org_id)) + if err != nil { + return results, fmt.Errorf("contacts from organization id: %w", err) + } + contact_ids := make([]int64, len(rows)) + for i, row := range rows { + contact_ids[i] = int64(row.ID) + } + contact_emails_by_contact_id, err := querycomms.ContactEmailByContactIDs(ctx, txn, contact_ids) + if err != nil { + return results, fmt.Errorf("by contact ids: %w", err) + } + contact_phones_by_contact_id, err := querycomms.ContactPhoneByContactIDs(ctx, txn, contact_ids) + if err != nil { + return results, fmt.Errorf("by contact ids: %w", err) + } + + results = make([]types.Contact, len(rows)) + for i, row := range rows { + contact_emails := contact_emails_by_contact_id[int64(row.ID)] + emails := make([]string, len(contact_emails)) + for i, e := range contact_emails { + emails[i] = e.Address + } + contact_phones := contact_phones_by_contact_id[int64(row.ID)] + phones := make([]types.Phone, len(contact_phones)) + for i, p := range contact_phones { + phones[i] = types.Phone{ + E164: p.E164, + CanSMS: p.CanSms, + } + } + results[i] = types.Contact{ + Emails: emails, + ID: row.ID, + Name: row.Name, + Phones: phones, + } + } + return results, nil +} diff --git a/platform/mailer.go b/platform/mailer.go index 39e8d0cb..eace089e 100644 --- a/platform/mailer.go +++ b/platform/mailer.go @@ -2,7 +2,6 @@ package platform import ( "context" - "fmt" "github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob/dialect/psql" @@ -10,34 +9,12 @@ import ( "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" + querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public" "github.com/Gleipnir-Technology/nidus-sync/platform/types" - "github.com/stephenafamo/scan" ) -func MailerByID(ctx context.Context, user User, id int32) (*types.Mailer, error) { - query := mailerQuery() - query.Apply( - sm.Where(models.ComplianceReportRequests.Columns.ID.EQ(psql.Arg(id))), - sm.Where( - models.Sites.Columns.OrganizationID.EQ(psql.Arg(user.Organization.ID)), - ), - ) - mailers, err := mailerQueryToRows(ctx, query) - if err != nil { - return nil, err - } - return mailers[id], nil -} -func MailerList(ctx context.Context, user User, limit int) ([]*types.Mailer, error) { - query := mailerQuery() - query.Apply( - sm.Where( - models.Sites.Columns.OrganizationID.EQ(psql.Arg(user.Organization.ID)), - ), - sm.OrderBy(models.ComplianceReportRequests.Columns.Created), - sm.Limit(limit), - ) - return mailerQueryToRows(ctx, query) +func MailerList(ctx context.Context, user User, limit int) ([]types.Mailer, error) { + return querypublic.MailersFromOrganizationID(ctx, db.PGInstance.PGXPool, int64(user.Organization.ID), int64(limit)) } func mailerQuery() bob.BaseQuery[*dialect.SelectQuery] { return psql.Select( @@ -78,11 +55,3 @@ func mailerQuery() bob.BaseQuery[*dialect.SelectQuery] { ), ) } -func mailerQueryToRows(ctx context.Context, query bob.BaseQuery[*dialect.SelectQuery]) ([]*types.Mailer, error) { - rows, err := bob.All(ctx, db.PGInstance.BobDB, query, scan.StructMapper[*types.Mailer]()) - if err != nil { - return nil, fmt.Errorf("query mailers: %w", err) - } - - return rows, nil -} diff --git a/platform/publicreport/report.go b/platform/publicreport/report.go index 704ff68a..e167a76c 100644 --- a/platform/publicreport/report.go +++ b/platform/publicreport/report.go @@ -143,13 +143,13 @@ func reportQueryToRows(ctx context.Context, reports []modelpublicreport.Report, DistrictID: &row.OrganizationID, District: nil, PublicID: row.PublicID, - Reporter: types.Contact{ - CanSMS: &row.ReporterPhoneCanSms, - Email: &row.ReporterEmail, - HasEmail: row.ReporterEmail != "", - HasPhone: row.ReporterPhone != "", - Name: row.ReporterName, - Phone: &row.ReporterPhone, + Reporter: types.ContactReporter{ + Email: row.ReporterEmail, + Name: row.ReporterName, + Phone: types.PhoneReporter{ + CanSMS: row.ReporterPhoneCanSms, + Number: row.ReporterPhone, + }, }, Status: row.Status.String(), Type: row.ReportType.String(), diff --git a/platform/types/contact.go b/platform/types/contact.go index be045d82..3242a92f 100644 --- a/platform/types/contact.go +++ b/platform/types/contact.go @@ -1,37 +1,32 @@ package types import ( - "encoding/json" - //"github.com/rs/zerolog/log" +//"github.com/rs/zerolog/log" ) type Contact struct { - CanSMS *bool `db:"can_sms" json:"can_sms"` - Email *string `db:"email" json:"email"` - HasEmail bool `json:"has_email"` - HasPhone bool `json:"has_phone"` - Name string `db:"name" json:"name"` - Phone *string `db:"phone" json:"phone"` + Emails []string `json:"emails"` + ID int32 `json:"-"` + Name string `json:"name"` + Phones []Phone `json:"phones"` +} +type ContactReporter struct { + Email string `json:"email"` + Name string `json:"name"` + Phone PhoneReporter `json:"phone"` +} +type Phone struct { + E164 string `json:"e164"` + CanSMS bool `json:"can_sms"` +} +type PhoneReporter struct { + CanSMS bool `json:"can_sms"` + Number string `json:"number"` } -func (c Contact) MarshalJSON() ([]byte, error) { - to_marshal := make(map[string]interface{}, 0) - if c.CanSMS != nil { - to_marshal["can_sms"] = *c.CanSMS - } - to_marshal["name"] = c.Name - to_marshal["has_email"] = (c.Email != nil && *c.Email != "") - to_marshal["has_phone"] = (c.Phone != nil && *c.Phone != "") - if c.Email != nil { - to_marshal["email"] = *c.Email - } else { - to_marshal["email"] = "" - } - if c.Phone != nil { - to_marshal["phone"] = *c.Phone - } else { - to_marshal["phone"] = "" - } - //log.Debug().Msg("marshaling contact") - return json.Marshal(to_marshal) +/* +func ContactFromModel(m model.Contact) Contact { + return Contact{ + Emails: } +*/ diff --git a/platform/types/publicreport.go b/platform/types/publicreport.go index 12c8cc33..2b508ee4 100644 --- a/platform/types/publicreport.go +++ b/platform/types/publicreport.go @@ -15,7 +15,7 @@ type PublicReport struct { DistrictID *int32 `db:"organization_id" json:"-"` District *string `db:"-" json:"district"` PublicID string `db:"public_id" json:"public_id"` - Reporter Contact `db:"reporter" json:"reporter"` + Reporter ContactReporter `db:"reporter" json:"reporter"` Status string `db:"status" json:"status"` Type string `db:"report_type" json:"type"` URI string `db:"-" json:"uri"` diff --git a/platform/types/site.go b/platform/types/site.go index ae66811c..146b25e0 100644 --- a/platform/types/site.go +++ b/platform/types/site.go @@ -40,8 +40,11 @@ func SiteFromModel(s *models.Site) Site { Notes: s.Notes, OrganizationID: s.OrganizationID, Owner: Contact{ - Name: s.OwnerName, - Phone: &owner_phone, + Name: s.OwnerName, + Phones: []Phone{Phone{ + E164: owner_phone, + CanSMS: false, + }}, }, ResidentOwned: resident_owned, //ParcelID: s.ParcelID, diff --git a/resource/contact.go b/resource/contact.go new file mode 100644 index 00000000..88df0b59 --- /dev/null +++ b/resource/contact.go @@ -0,0 +1,48 @@ +package resource + +import ( + "context" + "net/http" + + 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/rs/zerolog/log" +) + +type contactR struct { + router *router +} + +func Contact(r *router) *contactR { + return &contactR{ + router: r, + } +} + +type contact struct { + types.Contact + URI string +} + +func (res *contactR) ByIDGet(ctx context.Context, r *http.Request, user platform.User, qp QueryParams) (contact, *nhttp.ErrorWithStatus) { + return contact{}, nil +} +func (res *contactR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]contact, *nhttp.ErrorWithStatus) { + contacts, err := platform.ContactsForOrganization(ctx, user.Organization.ID) + if err != nil { + return nil, nhttp.NewError("nuisance report query: %w", err) + } + result := make([]contact, len(contacts)) + for i, c := range contacts { + uri, err := res.router.IDToURI("contact.ByIDGet", int(c.ID)) + if err != nil { + return nil, nhttp.NewError("contact uri: %w", err) + } + result[i] = contact{ + Contact: c, + URI: uri, + } + } + return result, nil +} diff --git a/ts/components/CardPublicReport.vue b/ts/components/CardPublicReport.vue index e4610199..b0f26a0b 100644 --- a/ts/components/CardPublicReport.vue +++ b/ts/components/CardPublicReport.vue @@ -61,21 +61,21 @@
diff --git a/ts/components/CommunicationColumnAction.vue b/ts/components/CommunicationColumnAction.vue index 38be11bf..5161b97d 100644 --- a/ts/components/CommunicationColumnAction.vue +++ b/ts/components/CommunicationColumnAction.vue @@ -78,8 +78,8 @@
diff --git a/ts/components/PublicReportCardWater.vue b/ts/components/PublicReportCardWater.vue index ce40e152..8cec6567 100644 --- a/ts/components/PublicReportCardWater.vue +++ b/ts/components/PublicReportCardWater.vue @@ -87,13 +87,13 @@
@@ -92,7 +92,7 @@