Make it possible to save SMS support status on phone record

This commit is contained in:
Eli Ribble 2026-04-13 22:07:56 +00:00
parent 96878f24de
commit 9c557a0391
No known key found for this signature in database
14 changed files with 135 additions and 54 deletions

View file

@ -42,6 +42,15 @@ var CommsPhones = Table[
Generated: false,
AutoIncr: false,
},
CanSMS: column{
Name: "can_sms",
DBType: "boolean",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
},
Indexes: commsPhoneIndexes{
PhonePkey: index{
@ -75,11 +84,12 @@ type commsPhoneColumns struct {
E164 column
IsSubscribed column
Status column
CanSMS column
}
func (c commsPhoneColumns) AsSlice() []column {
return []column{
c.E164, c.IsSubscribed, c.Status,
c.E164, c.IsSubscribed, c.Status, c.CanSMS,
}
}

View file

@ -222,6 +222,15 @@ var PublicreportReports = Table[
Generated: false,
AutoIncr: false,
},
ReporterPhoneCanSMS: column{
Name: "reporter_phone_can_sms",
DBType: "boolean",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
},
Indexes: publicreportReportIndexes{
ReportPkey: index{
@ -337,11 +346,12 @@ type publicreportReportColumns struct {
LocationLongitude column
AddressGid column
ClientUUID column
ReporterPhoneCanSMS column
}
func (c publicreportReportColumns) AsSlice() []column {
return []column{
c.AddressRaw, c.AddressID, c.Created, c.Location, c.H3cell, c.ID, c.LatlngAccuracyType, c.LatlngAccuracyValue, c.MapZoom, c.OrganizationID, c.PublicID, c.ReporterName, c.ReporterEmail, c.ReporterPhone, c.ReporterContactConsent, c.ReportType, c.Reviewed, c.ReviewerID, c.Status, c.LocationLatitude, c.LocationLongitude, c.AddressGid, c.ClientUUID,
c.AddressRaw, c.AddressID, c.Created, c.Location, c.H3cell, c.ID, c.LatlngAccuracyType, c.LatlngAccuracyValue, c.MapZoom, c.OrganizationID, c.PublicID, c.ReporterName, c.ReporterEmail, c.ReporterPhone, c.ReporterContactConsent, c.ReportType, c.Reviewed, c.ReviewerID, c.Status, c.LocationLatitude, c.LocationLongitude, c.AddressGid, c.ClientUUID, c.ReporterPhoneCanSMS,
}
}

View file

@ -0,0 +1,10 @@
-- +goose Up
ALTER TABLE comms.phone ADD COLUMN can_sms BOOLEAN;
UPDATE comms.phone SET can_sms = TRUE;
ALTER TABLE comms.phone ALTER COLUMN can_sms SET NOT NULL;
ALTER TABLE publicreport.report ADD COLUMN reporter_phone_can_sms BOOLEAN;
UPDATE publicreport.report SET reporter_phone_can_sms = TRUE;
ALTER TABLE publicreport.report ALTER COLUMN reporter_phone_can_sms SET NOT NULL;
-- +goose Down
ALTER TABLE comms.phone DROP COLUMN can_sms;
ALTER TABLE publicreport.report DROAP COLUMN reporter_phone_can_sms;

View file

@ -28,6 +28,7 @@ type CommsPhone struct {
E164 string `db:"e164,pk" `
IsSubscribed bool `db:"is_subscribed" `
Status enums.CommsPhonestatustype `db:"status" `
CanSMS bool `db:"can_sms" `
R commsPhoneR `db:"-" `
}
@ -60,12 +61,13 @@ type commsPhoneR struct {
func buildCommsPhoneColumns(alias string) commsPhoneColumns {
return commsPhoneColumns{
ColumnsExpr: expr.NewColumnsExpr(
"e164", "is_subscribed", "status",
"e164", "is_subscribed", "status", "can_sms",
).WithParent("comms.phone"),
tableAlias: alias,
E164: psql.Quote(alias, "e164"),
IsSubscribed: psql.Quote(alias, "is_subscribed"),
Status: psql.Quote(alias, "status"),
CanSMS: psql.Quote(alias, "can_sms"),
}
}
@ -75,6 +77,7 @@ type commsPhoneColumns struct {
E164 psql.Expression
IsSubscribed psql.Expression
Status psql.Expression
CanSMS psql.Expression
}
func (c commsPhoneColumns) Alias() string {
@ -92,10 +95,11 @@ type CommsPhoneSetter struct {
E164 omit.Val[string] `db:"e164,pk" `
IsSubscribed omit.Val[bool] `db:"is_subscribed" `
Status omit.Val[enums.CommsPhonestatustype] `db:"status" `
CanSMS omit.Val[bool] `db:"can_sms" `
}
func (s CommsPhoneSetter) SetColumns() []string {
vals := make([]string, 0, 3)
vals := make([]string, 0, 4)
if s.E164.IsValue() {
vals = append(vals, "e164")
}
@ -105,6 +109,9 @@ func (s CommsPhoneSetter) SetColumns() []string {
if s.Status.IsValue() {
vals = append(vals, "status")
}
if s.CanSMS.IsValue() {
vals = append(vals, "can_sms")
}
return vals
}
@ -118,6 +125,9 @@ func (s CommsPhoneSetter) Overwrite(t *CommsPhone) {
if s.Status.IsValue() {
t.Status = s.Status.MustGet()
}
if s.CanSMS.IsValue() {
t.CanSMS = s.CanSMS.MustGet()
}
}
func (s *CommsPhoneSetter) Apply(q *dialect.InsertQuery) {
@ -126,7 +136,7 @@ func (s *CommsPhoneSetter) Apply(q *dialect.InsertQuery) {
})
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 3)
vals := make([]bob.Expression, 4)
if s.E164.IsValue() {
vals[0] = psql.Arg(s.E164.MustGet())
} else {
@ -145,6 +155,12 @@ func (s *CommsPhoneSetter) Apply(q *dialect.InsertQuery) {
vals[2] = psql.Raw("DEFAULT")
}
if s.CanSMS.IsValue() {
vals[3] = psql.Arg(s.CanSMS.MustGet())
} else {
vals[3] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
@ -154,7 +170,7 @@ func (s CommsPhoneSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
}
func (s CommsPhoneSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 3)
exprs := make([]bob.Expression, 0, 4)
if s.E164.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
@ -177,6 +193,13 @@ func (s CommsPhoneSetter) Expressions(prefix ...string) []bob.Expression {
}})
}
if s.CanSMS.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "can_sms")...),
psql.Arg(s.CanSMS),
}})
}
return exprs
}
@ -1421,6 +1444,7 @@ type commsPhoneWhere[Q psql.Filterable] struct {
E164 psql.WhereMod[Q, string]
IsSubscribed psql.WhereMod[Q, bool]
Status psql.WhereMod[Q, enums.CommsPhonestatustype]
CanSMS psql.WhereMod[Q, bool]
}
func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] {
@ -1432,6 +1456,7 @@ func buildCommsPhoneWhere[Q psql.Filterable](cols commsPhoneColumns) commsPhoneW
E164: psql.Where[Q, string](cols.E164),
IsSubscribed: psql.Where[Q, bool](cols.IsSubscribed),
Status: psql.Where[Q, enums.CommsPhonestatustype](cols.Status),
CanSMS: psql.Where[Q, bool](cols.CanSMS),
}
}

View file

@ -51,6 +51,7 @@ type PublicreportReport struct {
LocationLongitude null.Val[float64] `db:"location_longitude,generated" `
AddressGid string `db:"address_gid" `
ClientUUID null.Val[uuid.UUID] `db:"client_uuid" `
ReporterPhoneCanSMS bool `db:"reporter_phone_can_sms" `
R publicreportReportR `db:"-" `
}
@ -86,7 +87,7 @@ type publicreportReportR struct {
func buildPublicreportReportColumns(alias string) publicreportReportColumns {
return publicreportReportColumns{
ColumnsExpr: expr.NewColumnsExpr(
"address_raw", "address_id", "created", "location", "h3cell", "id", "latlng_accuracy_type", "latlng_accuracy_value", "map_zoom", "organization_id", "public_id", "reporter_name", "reporter_email", "reporter_phone", "reporter_contact_consent", "report_type", "reviewed", "reviewer_id", "status", "location_latitude", "location_longitude", "address_gid", "client_uuid",
"address_raw", "address_id", "created", "location", "h3cell", "id", "latlng_accuracy_type", "latlng_accuracy_value", "map_zoom", "organization_id", "public_id", "reporter_name", "reporter_email", "reporter_phone", "reporter_contact_consent", "report_type", "reviewed", "reviewer_id", "status", "location_latitude", "location_longitude", "address_gid", "client_uuid", "reporter_phone_can_sms",
).WithParent("publicreport.report"),
tableAlias: alias,
AddressRaw: psql.Quote(alias, "address_raw"),
@ -112,6 +113,7 @@ func buildPublicreportReportColumns(alias string) publicreportReportColumns {
LocationLongitude: psql.Quote(alias, "location_longitude"),
AddressGid: psql.Quote(alias, "address_gid"),
ClientUUID: psql.Quote(alias, "client_uuid"),
ReporterPhoneCanSMS: psql.Quote(alias, "reporter_phone_can_sms"),
}
}
@ -141,6 +143,7 @@ type publicreportReportColumns struct {
LocationLongitude psql.Expression
AddressGid psql.Expression
ClientUUID psql.Expression
ReporterPhoneCanSMS psql.Expression
}
func (c publicreportReportColumns) Alias() string {
@ -176,10 +179,11 @@ type PublicreportReportSetter struct {
Status omit.Val[enums.PublicreportReportstatustype] `db:"status" `
AddressGid omit.Val[string] `db:"address_gid" `
ClientUUID omitnull.Val[uuid.UUID] `db:"client_uuid" `
ReporterPhoneCanSMS omit.Val[bool] `db:"reporter_phone_can_sms" `
}
func (s PublicreportReportSetter) SetColumns() []string {
vals := make([]string, 0, 21)
vals := make([]string, 0, 22)
if s.AddressRaw.IsValue() {
vals = append(vals, "address_raw")
}
@ -243,6 +247,9 @@ func (s PublicreportReportSetter) SetColumns() []string {
if !s.ClientUUID.IsUnset() {
vals = append(vals, "client_uuid")
}
if s.ReporterPhoneCanSMS.IsValue() {
vals = append(vals, "reporter_phone_can_sms")
}
return vals
}
@ -310,6 +317,9 @@ func (s PublicreportReportSetter) Overwrite(t *PublicreportReport) {
if !s.ClientUUID.IsUnset() {
t.ClientUUID = s.ClientUUID.MustGetNull()
}
if s.ReporterPhoneCanSMS.IsValue() {
t.ReporterPhoneCanSMS = s.ReporterPhoneCanSMS.MustGet()
}
}
func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) {
@ -318,7 +328,7 @@ func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) {
})
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 21)
vals := make([]bob.Expression, 22)
if s.AddressRaw.IsValue() {
vals[0] = psql.Arg(s.AddressRaw.MustGet())
} else {
@ -445,6 +455,12 @@ func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) {
vals[20] = psql.Raw("DEFAULT")
}
if s.ReporterPhoneCanSMS.IsValue() {
vals[21] = psql.Arg(s.ReporterPhoneCanSMS.MustGet())
} else {
vals[21] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
@ -454,7 +470,7 @@ func (s PublicreportReportSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
}
func (s PublicreportReportSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 21)
exprs := make([]bob.Expression, 0, 22)
if s.AddressRaw.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
@ -603,6 +619,13 @@ func (s PublicreportReportSetter) Expressions(prefix ...string) []bob.Expression
}})
}
if s.ReporterPhoneCanSMS.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "reporter_phone_can_sms")...),
psql.Arg(s.ReporterPhoneCanSMS),
}})
}
return exprs
}
@ -2021,6 +2044,7 @@ type publicreportReportWhere[Q psql.Filterable] struct {
LocationLongitude psql.WhereNullMod[Q, float64]
AddressGid psql.WhereMod[Q, string]
ClientUUID psql.WhereNullMod[Q, uuid.UUID]
ReporterPhoneCanSMS psql.WhereMod[Q, bool]
}
func (publicreportReportWhere[Q]) AliasedAs(alias string) publicreportReportWhere[Q] {
@ -2052,6 +2076,7 @@ func buildPublicreportReportWhere[Q psql.Filterable](cols publicreportReportColu
LocationLongitude: psql.WhereNull[Q, float64](cols.LocationLongitude),
AddressGid: psql.Where[Q, string](cols.AddressGid),
ClientUUID: psql.WhereNull[Q, uuid.UUID](cols.ClientUUID),
ReporterPhoneCanSMS: psql.Where[Q, bool](cols.ReporterPhoneCanSMS),
}
}

View file

@ -172,6 +172,7 @@ func reportQuery() bob.BaseQuery[*dialect.SelectQuery] {
"r.reporter_email AS \"reporter.email\"",
"r.reporter_name AS \"reporter.name\"",
"r.reporter_phone AS \"reporter.phone\"",
"r.reporter_phone_can_sms AS \"reporter.can_sms\"",
"r.status",
),
sm.From("publicreport.report").As("r"),

View file

@ -20,8 +20,9 @@ func EnsureInDB(ctx context.Context, txn bob.Executor, dst types.E164) (err erro
}
func ensureInDB(ctx context.Context, txn bob.Executor, destination string) (err error) {
_, err = psql.Insert(
im.Into("comms.phone", "e164", "is_subscribed", "status"),
im.Into("comms.phone", "can_sms", "e164", "is_subscribed", "status"),
im.Values(
psql.Arg(true),
psql.Arg(destination),
psql.Arg(false),
psql.Arg("unconfirmed"),

View file

@ -6,15 +6,19 @@ import (
)
type Contact struct {
Email *string `db:"email" json:"-"`
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 *string `db:"phone" json:"phone"`
}
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 != "")

View file

@ -63,6 +63,7 @@ func (res *complianceR) Create(ctx context.Context, r *http.Request, n publicrep
ReporterEmail: omit.From(""),
ReporterName: omit.From(""),
ReporterPhone: omit.From(""),
ReporterPhoneCanSMS: omit.FromPtr[bool](nil),
ReportType: omit.From(enums.PublicreportReporttypeCompliance),
Status: omit.From(enums.PublicreportReportstatustypeReported),
}
@ -74,7 +75,6 @@ func (res *complianceR) Create(ctx context.Context, r *http.Request, n publicrep
HasDog: omitnull.FromPtr[bool](nil),
PermissionType: omit.From(enums.PermissionaccesstypeUnselected),
//ReportID omit.Val[int32]
ReportPhoneCanText: omitnull.FromPtr[bool](nil),
WantsScheduled: omitnull.FromPtr[bool](nil),
}
report, err := platform.PublicReportComplianceCreate(ctx, setter_report, setter_compliance)
@ -107,7 +107,7 @@ type publicreportComplianceForm struct {
Location omit.Val[types.Location] `schema:"location" json:"location"`
PermissionType omit.Val[enums.Permissionaccesstype] `schema:"permission_type" json:"permission_type"`
Reporter omit.Val[types.Contact] `schema:"reporter" json:"reporter"`
ReportPhoneCanText omitnull.Val[bool] `schema:"report_phone_can_text" json:"report_phone_can_text"`
ReportPhoneCanSMS omitnull.Val[bool] `schema:"report_phone_can_text" json:"report_phone_can_text"`
WantsScheduled omitnull.Val[bool] `schema:"wants_scheduled" json:"wants_scheduled"`
}
@ -118,6 +118,7 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicr
return nil, nhttp.NewBadRequest("You must provide an ID")
}
report_setter := models.PublicreportReportSetter{}
compliance_setter := models.PublicreportComplianceSetter{}
var location *types.Location
if prf.Location.IsValue() {
l := prf.Location.MustGet()
@ -137,13 +138,15 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicr
if reporter.Phone != nil {
report_setter.ReporterPhone = omit.From(*reporter.Phone)
}
if reporter.CanSMS != nil {
report_setter.ReporterPhoneCanSMS = omit.FromPtr(reporter.CanSMS)
}
}
var address *types.Address
if prf.Address.IsValue() {
a := prf.Address.MustGet()
address = &a
}
compliance_setter := models.PublicreportComplianceSetter{}
if prf.AccessInstructions.IsValue() {
compliance_setter.AccessInstructions = prf.AccessInstructions
}
@ -162,9 +165,6 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicr
if prf.PermissionType.IsValue() {
compliance_setter.PermissionType = prf.PermissionType
}
if prf.ReportPhoneCanText.IsValue() {
compliance_setter.ReportPhoneCanText = prf.ReportPhoneCanText
}
if prf.WantsScheduled.IsValue() {
compliance_setter.WantsScheduled = prf.WantsScheduled
}

View file

@ -167,16 +167,8 @@ const poolLocation = ref<Location>({
latitude: 0,
longitude: 0,
});
const siteOwner = ref<Contact>({
has_email: false,
has_phone: false,
name: "",
});
const siteResident = ref<Contact>({
has_email: false,
has_phone: false,
name: "",
});
const siteOwner = ref<Contact>(new Contact());
const siteResident = ref<Contact>(new Contact());
const session = useSessionStore();
function doPoolLocation(event: MapClickEvent) {
console.log("pool location", event);

View file

@ -72,7 +72,7 @@
class="form-check-input"
type="checkbox"
id="can-text"
v-model="modelValue.reporter.can_text"
v-model="modelValue.reporter.can_sms"
/>
<label class="form-check-label" for="can-text">
You may send text messages to this number
@ -89,6 +89,14 @@
Email Address
<span class="optional-badge">(Optional)</span>
</label>
<div
class="alert alert-primary"
role="alert"
v-if="modelValue.reporter.has_email"
>
You've already added an email address to this report. If you alter the
email below, it will replace the current email address.
</div>
<input
type="email"
class="form-control"
@ -125,16 +133,10 @@
import { ref } from "vue";
import { router } from "@/rmo/router";
import type { District, PublicReport } from "@/type/api";
import type { Contact, District, PublicReport } from "@/type/api";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
export interface Contact {
name: string;
phone: string;
can_text: boolean;
email: string;
}
interface Emits {
(e: "doContact"): void;
(e: "update:modelValue", value: PublicReport): void;

View file

@ -198,7 +198,7 @@
<div class="summary-label">Phone</div>
<div class="summary-value" v-if="modelValue.reporter?.phone">
{{ modelValue.reporter.phone }}
<small class="text-muted" v-if="modelValue.reporter?.can_text"
<small class="text-muted" v-if="modelValue.reporter?.can_sms"
>(texting OK)</small
>
</div>

View file

@ -56,8 +56,7 @@ import {
PublicReportCompliance,
PublicReportComplianceOptions,
} from "@/type/api";
import { Address, Location, PermissionType } from "@/type/api";
import { type Contact } from "@/rmo/content/compliance/Contact.vue";
import { Contact, Address, Location, PermissionType } from "@/type/api";
interface Props {
slug: string;
@ -129,6 +128,9 @@ function doContact() {
return;
}
console.log("contact", report.value.reporter);
updateReport({
reporter: report.value.reporter,
});
}
function doPermission() {
if (!report.value) {

View file

@ -36,7 +36,7 @@ export interface Bounds {
max: Location;
}
export interface ContactOptions {
can_text?: boolean;
can_sms: boolean;
email?: string;
has_email: boolean;
has_phone: boolean;
@ -44,14 +44,14 @@ export interface ContactOptions {
phone?: string;
}
export class Contact {
can_text?: boolean;
email?: string;
can_sms: boolean;
email: string;
has_email: boolean;
has_phone: boolean;
name?: string;
phone?: string;
name: string;
phone: string;
constructor(options?: ContactOptions) {
this.can_text = options?.can_text ?? false;
this.can_sms = options?.can_sms ?? false;
this.email = options?.email ?? "";
this.has_email = options?.has_email ?? false;
this.has_phone = options?.has_phone ?? false;
@ -159,7 +159,6 @@ export interface ComplianceUpdate {
permission_type?: string;
reporter?: Contact;
//uri: string;
report_phone_can_text?: boolean;
wants_scheduled?: boolean;
}
export interface PublicReportDTO {
@ -370,7 +369,7 @@ export class PublicReportWater extends PublicReport {
contact: {
name: "",
phone: "",
can_text: true,
can_sms: true,
email: "",
},
id: "",