WIP: creating contact resource
Some checks failed
/ golint (push) Has been cancelled

This commit is contained in:
Eli Ribble 2026-05-15 20:10:14 +00:00
parent 8b203908a0
commit 725945d95c
No known key found for this signature in database
22 changed files with 381 additions and 135 deletions

View file

@ -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 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 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 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 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 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) type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus)

View file

@ -48,6 +48,7 @@ func AddRoutesRMO(r *mux.Router) {
func AddRoutesSync(r *mux.Router) { func AddRoutesSync(r *mux.Router) {
router := resource.NewRouter(r) router := resource.NewRouter(r)
contact := resource.Contact(router)
compliance_request := resource.ComplianceRequest(router) compliance_request := resource.ComplianceRequest(router)
district := resource.District(router) district := resource.District(router)
geocode := resource.Geocode(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.Handle("/compliance-request/mailer", authenticatedHandlerJSONPost(compliance_request.CreateMailer)).Methods("POST")
//r.HandleFunc("/compliance-request/image/pool/{public_id}", getComplianceRequestImagePool).Methods("GET") //r.HandleFunc("/compliance-request/image/pool/{public_id}", getComplianceRequestImagePool).Methods("GET")
r.Handle("/configuration/integration/arcgis", authenticatedHandlerJSONPost(postConfigurationIntegrationArcgis)).Methods("POST") 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) email := resource.Email(router)
r.Handle("/email/{id}", authenticatedHandlerJSON(email.Get)).Methods("GET").Name("email.ByIDGet") r.Handle("/email/{id}", authenticatedHandlerJSON(email.Get)).Methods("GET").Name("email.ByIDGet")
r.Handle("/events", auth.NewEnsureAuth(streamEvents)).Methods("GET") r.Handle("/events", auth.NewEnsureAuth(streamEvents)).Methods("GET")

View file

@ -35,13 +35,10 @@ func ContactUpdateName(ctx context.Context, txn db.Ex, id int64, name string) er
return db.ExecuteNoneTx(ctx, txn, statement) return db.ExecuteNoneTx(ctx, txn, statement)
} }
/* func ContactsFromOrganizationID(ctx context.Context, txn db.Ex, org_id int64) ([]model.Contact, error) {
func ContactsFromAddress(ctx context.Context, address string) ([]model.Contact, error) {
statement := table.Contact.SELECT( statement := table.Contact.SELECT(
table.Contact.AllColumns, table.Contact.AllColumns,
).FROM(table.Contact). ).FROM(table.Contact).
WHERE(table.Contact.Source.EQ(postgres.String(address)).OR( WHERE(table.Contact.OrganizationID.EQ(postgres.Int(org_id)))
table.Contact.Destination.EQ(postgres.String(address)))) return db.ExecuteManyTx[model.Contact](ctx, txn, statement)
return db.ExecuteMany[model.Contact](ctx, statement)
} }
*/

View file

@ -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
}

View file

@ -2,6 +2,7 @@ package comms
import ( import (
"context" "context"
"fmt"
//"github.com/Gleipnir-Technology/bob" //"github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/nidus-sync/db" "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))) WHERE(table.ContactPhone.E164.EQ(postgres.String(e164)))
return db.ExecuteNoneTx(ctx, txn, statement) return db.ExecuteNoneTx(ctx, txn, statement)
} }
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))
func ContactPhonesFromAddress(ctx context.Context, address string) ([]model.ContactPhone, error) { for i, contact_id := range contact_ids {
sql_ids[i] = postgres.Int(contact_id)
}
statement := table.ContactPhone.SELECT( statement := table.ContactPhone.SELECT(
table.ContactPhone.AllColumns, table.ContactPhone.AllColumns,
).FROM(table.ContactPhone). ).FROM(table.ContactPhone).
WHERE(table.ContactPhone.Source.EQ(postgres.String(address)).OR( WHERE(table.ContactPhone.ContactID.IN(sql_ids...))
table.ContactPhone.Destination.EQ(postgres.String(address)))) rows, err := db.ExecuteManyTx[model.ContactPhone](ctx, txn, statement)
return db.ExecuteMany[model.ContactPhone](ctx, 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
} }
*/

70
db/query/public/mailer.go Normal file
View file

@ -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)
}

54
platform/contact.go Normal file
View file

@ -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
}

View file

@ -2,7 +2,6 @@ package platform
import ( import (
"context" "context"
"fmt"
"github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql"
@ -10,34 +9,12 @@ import (
"github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "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/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/stephenafamo/scan"
) )
func MailerByID(ctx context.Context, user User, id int32) (*types.Mailer, error) { func MailerList(ctx context.Context, user User, limit int) ([]types.Mailer, error) {
query := mailerQuery() return querypublic.MailersFromOrganizationID(ctx, db.PGInstance.PGXPool, int64(user.Organization.ID), int64(limit))
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 mailerQuery() bob.BaseQuery[*dialect.SelectQuery] { func mailerQuery() bob.BaseQuery[*dialect.SelectQuery] {
return psql.Select( 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
}

View file

@ -143,13 +143,13 @@ func reportQueryToRows(ctx context.Context, reports []modelpublicreport.Report,
DistrictID: &row.OrganizationID, DistrictID: &row.OrganizationID,
District: nil, District: nil,
PublicID: row.PublicID, PublicID: row.PublicID,
Reporter: types.Contact{ Reporter: types.ContactReporter{
CanSMS: &row.ReporterPhoneCanSms, Email: row.ReporterEmail,
Email: &row.ReporterEmail, Name: row.ReporterName,
HasEmail: row.ReporterEmail != "", Phone: types.PhoneReporter{
HasPhone: row.ReporterPhone != "", CanSMS: row.ReporterPhoneCanSms,
Name: row.ReporterName, Number: row.ReporterPhone,
Phone: &row.ReporterPhone, },
}, },
Status: row.Status.String(), Status: row.Status.String(),
Type: row.ReportType.String(), Type: row.ReportType.String(),

View file

@ -1,37 +1,32 @@
package types package types
import ( import (
"encoding/json" //"github.com/rs/zerolog/log"
//"github.com/rs/zerolog/log"
) )
type Contact struct { type Contact struct {
CanSMS *bool `db:"can_sms" json:"can_sms"` Emails []string `json:"emails"`
Email *string `db:"email" json:"email"` ID int32 `json:"-"`
HasEmail bool `json:"has_email"` Name string `json:"name"`
HasPhone bool `json:"has_phone"` Phones []Phone `json:"phones"`
Name string `db:"name" json:"name"` }
Phone *string `db:"phone" json:"phone"` 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) func ContactFromModel(m model.Contact) Contact {
if c.CanSMS != nil { return Contact{
to_marshal["can_sms"] = *c.CanSMS Emails:
}
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)
} }
*/

View file

@ -15,7 +15,7 @@ type PublicReport struct {
DistrictID *int32 `db:"organization_id" json:"-"` DistrictID *int32 `db:"organization_id" json:"-"`
District *string `db:"-" json:"district"` District *string `db:"-" json:"district"`
PublicID string `db:"public_id" json:"public_id"` 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"` Status string `db:"status" json:"status"`
Type string `db:"report_type" json:"type"` Type string `db:"report_type" json:"type"`
URI string `db:"-" json:"uri"` URI string `db:"-" json:"uri"`

View file

@ -40,8 +40,11 @@ func SiteFromModel(s *models.Site) Site {
Notes: s.Notes, Notes: s.Notes,
OrganizationID: s.OrganizationID, OrganizationID: s.OrganizationID,
Owner: Contact{ Owner: Contact{
Name: s.OwnerName, Name: s.OwnerName,
Phone: &owner_phone, Phones: []Phone{Phone{
E164: owner_phone,
CanSMS: false,
}},
}, },
ResidentOwned: resident_owned, ResidentOwned: resident_owned,
//ParcelID: s.ParcelID, //ParcelID: s.ParcelID,

48
resource/contact.go Normal file
View file

@ -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
}

View file

@ -61,21 +61,21 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label <label
v-if="report.reporter.has_email" v-if="report.reporter.emails.length"
class="form-label text-muted small mb-0" class="form-label text-muted small mb-0"
> >
<i class="bi bi-envelope"></i> <i class="bi bi-envelope"></i>
<a :href="'mailto:' + report.reporter.email">{{ <a :href="'mailto:' + report.reporter.emails[0]">{{
report.reporter.email report.reporter.emails[0]
}}</a> }}</a>
</label> </label>
<label <label
v-if="report.reporter.has_phone" v-if="report.reporter.phones.length"
class="form-label text-muted small mb-0" class="form-label text-muted small mb-0"
> >
<i class="bi bi-phone"></i> <i class="bi bi-phone"></i>
<a :href="'tel:+' + report.reporter.phone">{{ <a :href="'tel:+' + report.reporter.phones[0]">{{
report.reporter.phone report.reporter.phones[0]
}}</a> }}</a>
</label> </label>
</div> </div>

View file

@ -78,8 +78,8 @@
<div <div
v-if=" v-if="
!( !(
selectedReport?.reporter.has_email || selectedReport?.reporter.emails.length ||
selectedReport?.reporter.has_phone selectedReport?.reporter.phones.length
) )
" "
class="mb-3" class="mb-3"
@ -91,8 +91,8 @@
</div> </div>
<div <div
v-if=" v-if="
selectedReport?.reporter.has_email || selectedReport?.reporter.emails.length ||
selectedReport?.reporter.has_phone selectedReport?.reporter.phones.length
" "
class="mb-3" class="mb-3"
> >

View file

@ -87,13 +87,13 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label <label
v-if="report.owner.has_email" v-if="report.owner.emails.length"
class="form-label text-muted small mb-0" class="form-label text-muted small mb-0"
> >
<i class="bi bi-envelope"></i> <i class="bi bi-envelope"></i>
</label> </label>
<label <label
v-if="report.owner.has_phone" v-if="report.owner.phones.length"
class="form-label text-muted small mb-0" class="form-label text-muted small mb-0"
> >
<i class="bi bi-phone"></i> <i class="bi bi-phone"></i>

View file

@ -248,10 +248,8 @@ const hasCompleteResponse = computed(() => {
r.images.length > 0 || r.images.length > 0 ||
r.permission_type == PermissionType.GRANTED || r.permission_type == PermissionType.GRANTED ||
r.reporter.name || r.reporter.name ||
r.reporter.phone || r.reporter.phones.length ||
r.reporter.has_phone || r.reporter.emails.length
r.reporter.email ||
r.reporter.has_email
) { ) {
return true; return true;
} }

View file

@ -61,7 +61,7 @@
id="contact-phone" id="contact-phone"
name="phone" name="phone"
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
v-model="modelValue.reporter.phone" v-model="modelValue.reporter.phone.number"
/> />
</div> </div>
@ -92,7 +92,7 @@
<div <div
class="alert alert-primary" class="alert alert-primary"
role="alert" role="alert"
v-if="modelValue.reporter.has_email" v-if="modelValue.reporter.email != ''"
> >
You've already added an email address to this report. If you alter the You've already added an email address to this report. If you alter the
email below, it will replace the current email address. email below, it will replace the current email address.

View file

@ -199,7 +199,7 @@
<div <div
class="summary-value" class="summary-value"
v-if=" v-if="
modelValue.reporter?.phone || modelValue.reporter?.has_phone modelValue.reporter?.phone || modelValue.reporter?.phone != ''
" "
> >
{{ modelValue.reporter.phone }} {{ modelValue.reporter.phone }}
@ -216,7 +216,7 @@
<div <div
class="summary-value" class="summary-value"
v-if=" v-if="
modelValue.reporter?.email || modelValue.reporter?.has_email modelValue.reporter?.email || modelValue.reporter?.email != ''
" "
> >
{{ modelValue.reporter?.email }} {{ modelValue.reporter?.email }}

View file

@ -1,5 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { shallowRef } from "vue"; import { ref, shallowRef } from "vue";
import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { SSEManager, SSEMessageResource } from "@/SSEManager";
import { useSessionStore } from "@/store/session"; import { useSessionStore } from "@/store/session";
@ -8,6 +8,8 @@ import { apiClient } from "@/client";
import { import {
Communication, Communication,
type CommunicationDTO, type CommunicationDTO,
Contact,
type ContactDTO,
PublicReport, PublicReport,
type PublicReportDTO, type PublicReportDTO,
} from "@/type/api"; } from "@/type/api";
@ -22,6 +24,7 @@ function createResourceStore<dto, full extends uriHaver>(
from_json: jsonConverter<dto, full>, from_json: jsonConverter<dto, full>,
) { ) {
const _resourceByURI = shallowRef<Map<string, full>>(new Map()); const _resourceByURI = shallowRef<Map<string, full>>(new Map());
const _resourceFetchAll = ref<Promise<full[]> | null>(null);
const _resourceFetchByURI = shallowRef<Map<string, Promise<full> | null>>( const _resourceFetchByURI = shallowRef<Map<string, Promise<full> | null>>(
new Map(), new Map(),
); );
@ -33,20 +36,25 @@ function createResourceStore<dto, full extends uriHaver>(
} }
}); });
async function byAll(): Promise<full[]> {
const cur = _resourceFetchAll.value;
if (cur) {
return cur;
}
return fetchAll();
}
async function byID(id: string): Promise<full> { async function byID(id: string): Promise<full> {
const uri = uriFromID(id); const uri = uriFromID(id);
const cur = _resourceFetchByURI.value.get(uri); return byURI(uri);
if (cur) {
return cur;
}
return fetchByID(id);
} }
async function byURI(uri: string): Promise<full> { async function byURI(uri: string): Promise<full> {
const cur = _resourceFetchByURI.value.get(uri); let cur = _resourceFetchByURI.value.get(uri);
if (cur) { if (cur) {
return cur; return cur;
} }
return fetchByURI(uri); cur = fetchByURI(uri);
_resourceFetchByURI.value.set(uri, cur);
return cur;
} }
async function fetchAll(): Promise<full[]> { async function fetchAll(): Promise<full[]> {
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
@ -88,9 +96,12 @@ function createResourceStore<dto, full extends uriHaver>(
return `${api_base}/${id}`; return `${api_base}/${id}`;
} }
return { return {
byAll,
byID, byID,
byURI, byURI,
fetchAll, fetchAll,
fetchByID,
fetchByURI,
loadingURI, loadingURI,
}; };
} }
@ -101,6 +112,11 @@ export const useStoreResource = defineStore("resource", () => {
"/communication", "/communication",
Communication.fromJSON, Communication.fromJSON,
), ),
contact: createResourceStore<ContactDTO, Contact>(
"sync:contact",
"/contact",
Contact.fromJSON,
),
publicreport: createResourceStore<PublicReportDTO, PublicReport>( publicreport: createResourceStore<PublicReportDTO, PublicReport>(
"sync:publicreport", "sync:publicreport",
"/publicreport", "/publicreport",

View file

@ -77,28 +77,47 @@ export class Bounds {
} }
} }
export interface ContactOptions { export interface ContactOptions {
can_sms: boolean; emails?: string[];
email?: string;
has_email: boolean;
has_phone: boolean;
name?: string; name?: string;
phone?: string; phones?: Phone[];
uri?: string;
}
export interface Phone {
can_sms: boolean;
e164: string;
}
export interface PhoneReporter {
can_sms: boolean;
number: string;
} }
export class Contact { export class Contact {
can_sms: boolean; emails: string[];
email: string;
has_email: boolean;
has_phone: boolean;
name: string; name: string;
phone: string; phones: Phone[];
uri: string;
constructor(options?: ContactOptions) { constructor(options?: ContactOptions) {
this.can_sms = options?.can_sms ?? false; this.emails = options?.emails ?? [];
this.email = options?.email ?? "";
this.has_email = options?.has_email ?? false;
this.has_phone = options?.has_phone ?? false;
this.name = options?.name ?? ""; this.name = options?.name ?? "";
this.phone = options?.phone ?? ""; this.phones = options?.phones ?? [];
this.uri = options?.uri ?? "";
} }
static fromJSON(json: ContactDTO): Contact {
return new Contact(json);
}
}
export interface ContactDTO {
name: string;
emails: string[];
phones: Phone[];
uri: string;
}
export class ContactReporter {
constructor(
public name: string,
public email: string,
public phone: Phone,
public uri: string,
) {}
} }
export interface District { export interface District {
name: string; name: string;
@ -195,7 +214,7 @@ export interface ComplianceUpdate {
//images?: Image[]; //images?: Image[];
location?: Location; location?: Location;
permission_type?: string; permission_type?: string;
reporter?: Contact; reporter?: ContactReporter;
submitted?: string; submitted?: string;
//uri: string; //uri: string;
wants_scheduled?: boolean; wants_scheduled?: boolean;
@ -212,7 +231,7 @@ export interface PublicReportDTO {
location: Location; location: Location;
log: LogEntryDTO[]; log: LogEntryDTO[];
public_id: string; public_id: string;
reporter: Contact; reporter: ContactReporter;
status: string; status: string;
type: string; type: string;
uri: string; uri: string;
@ -224,7 +243,7 @@ export interface PublicReportUpdate {
images?: Image[]; images?: Image[];
location?: Location; location?: Location;
public_id?: string; public_id?: string;
reporter?: Contact; reporter?: ContactReporter;
status?: string; status?: string;
type?: string; type?: string;
uri?: string; uri?: string;
@ -242,7 +261,7 @@ export interface PublicReportOptions {
location: Location; location: Location;
log: LogEntry[]; log: LogEntry[];
public_id: string; public_id: string;
reporter: Contact; reporter: ContactReporter;
status: string; status: string;
type: string; type: string;
uri: string; uri: string;
@ -254,7 +273,7 @@ export class PublicReport {
images: Image[]; images: Image[];
log: LogEntry[]; log: LogEntry[];
public_id: string; public_id: string;
reporter: Contact; reporter: ContactReporter;
status: string; status: string;
type: string; type: string;
uri: string; uri: string;
@ -266,7 +285,7 @@ export class PublicReport {
this.images = options?.images ?? []; this.images = options?.images ?? [];
this.log = options?.log ?? []; this.log = options?.log ?? [];
this.public_id = options?.public_id ?? ""; this.public_id = options?.public_id ?? "";
this.reporter = options?.reporter ?? new Contact(); this.reporter = options?.reporter ?? new ContactReporter();
this.status = options?.status ?? ""; this.status = options?.status ?? "";
this.type = options?.type ?? ""; this.type = options?.type ?? "";
this.uri = options?.uri ?? ""; this.uri = options?.uri ?? "";

View file

@ -12,8 +12,16 @@
</ThreeColumn> </ThreeColumn>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computedAsync } from "@vueuse/core";
import ThreeColumn from "@/components/layout/ThreeColumn.vue"; import ThreeColumn from "@/components/layout/ThreeColumn.vue";
import ReviewContactColumnAction from "@/components/ReviewContactColumnAction.vue"; import ReviewContactColumnAction from "@/components/ReviewContactColumnAction.vue";
import ReviewContactColumnDetail from "@/components/ReviewContactColumnDetail.vue"; import ReviewContactColumnDetail from "@/components/ReviewContactColumnDetail.vue";
import ReviewContactColumnList from "@/components/ReviewContactColumnList.vue"; import ReviewContactColumnList from "@/components/ReviewContactColumnList.vue";
import { useStoreResource } from "@/store/resource";
const storeResource = useStoreResource();
const contacts = computedAsync(() => {
return storeResource.contact.byAll();
});
</script> </script>