This commit is contained in:
parent
8b203908a0
commit
725945d95c
22 changed files with 381 additions and 135 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
||||||
52
db/query/comms/contact_email.go
Normal file
52
db/query/comms/contact_email.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
70
db/query/public/mailer.go
Normal 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
54
platform/contact.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
|
||||||
|
|
@ -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
48
resource/contact.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue