Update nuisance submission to go to submitted page
This commit is contained in:
parent
c41154a200
commit
2c0bfb9904
31 changed files with 747 additions and 228 deletions
|
|
@ -73,10 +73,15 @@ func AddRoutes(r *mux.Router) {
|
|||
// Unauthenticated endpoints
|
||||
district := resource.District(router)
|
||||
r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET")
|
||||
r.Handle("/district/{id}", handlerJSON(district.GetByID)).Methods("GET").Name("district.ByIDGet")
|
||||
geocode := resource.Geocode(router)
|
||||
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
|
||||
r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST")
|
||||
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
|
||||
publicreport := resource.Publicreport(router)
|
||||
r.Handle("/publicreport/{id}", handlerJSON(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet")
|
||||
publicreport_notification := resource.PublicreportNotification(router)
|
||||
r.Handle("/publicreport-notification", handlerJSONPost(publicreport_notification.Create)).Methods("POST")
|
||||
|
||||
//r.HandleFunc("/district", apiGetDistrict).Methods("GET")
|
||||
r.HandleFunc("/district/{slug}/logo", apiGetDistrictLogo).Methods("GET").Name("district.logo.BySlug")
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ func (o Organization) ServiceRequestRecent(ctx context.Context) ([]*models.Field
|
|||
func (o Organization) Slug() string {
|
||||
return o.model.Slug.GetOr("")
|
||||
}
|
||||
func (o Organization) Website() string {
|
||||
return o.model.Website.GetOr("")
|
||||
}
|
||||
func OrganizationByID(ctx context.Context, id int) (*Organization, error) {
|
||||
org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, int32(id))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func PublicreportByID(ctx context.Context, report_id string) (*models.PublicreportReport, error) {
|
||||
report, err := models.PublicreportReports.Query(
|
||||
models.SelectWhere.PublicreportReports.PublicID.EQ(report_id),
|
||||
).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
func PublicreportInvalid(ctx context.Context, user User, report_id string) error {
|
||||
report, err := reportFromID(ctx, user, report_id)
|
||||
if err != nil {
|
||||
|
|
|
|||
72
platform/publicreport_notification.go
Normal file
72
platform/publicreport_notification.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PublicreportNotification struct {
|
||||
Consent bool
|
||||
Email string
|
||||
Name string
|
||||
Notification bool
|
||||
Phone *types.E164
|
||||
ReportID string
|
||||
Subscription bool
|
||||
}
|
||||
|
||||
func PublicreportNotificationCreate(ctx context.Context, pn PublicreportNotification) error {
|
||||
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin txn: %w", err)
|
||||
}
|
||||
defer txn.Rollback(ctx)
|
||||
rep, err := models.PublicreportReports.Query(
|
||||
models.SelectWhere.PublicreportReports.PublicID.EQ(pn.ReportID),
|
||||
).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find report '%s': %w", pn.ReportID, err)
|
||||
}
|
||||
|
||||
err = report.SaveReporter(ctx, txn, pn.ReportID, pn.Name, pn.Email, pn.Phone, pn.Consent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save reporter: %w", err)
|
||||
}
|
||||
if pn.Email != "" {
|
||||
if pn.Subscription {
|
||||
err = report.RegisterSubscriptionEmail(ctx, txn, pn.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register subscription email: %w", err)
|
||||
}
|
||||
}
|
||||
if pn.Notification {
|
||||
err = report.RegisterNotificationEmail(ctx, txn, pn.ReportID, pn.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register notification email: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if pn.Phone != nil {
|
||||
if pn.Subscription {
|
||||
err = report.RegisterSubscriptionPhone(ctx, txn, *pn.Phone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register subscription phone: %w", err)
|
||||
}
|
||||
}
|
||||
if pn.Notification {
|
||||
err = report.RegisterNotificationPhone(ctx, txn, pn.ReportID, *pn.Phone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register notification phone: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
txn.Commit(ctx)
|
||||
PublicReportReporterUpdated(ctx, rep.OrganizationID, pn.ReportID)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ErrorWithCode struct {
|
||||
code string
|
||||
err error
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *ErrorWithCode) Code() string {
|
||||
return e.code
|
||||
}
|
||||
func (e *ErrorWithCode) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func newInternalError(err error, format string, args ...any) *ErrorWithCode {
|
||||
log.Error().Err(err).Str("format", format).Msg("internal server error")
|
||||
return newErrorWithCode("internal-error", format, args...)
|
||||
}
|
||||
func newErrorWithCode(code string, format string, args ...any) *ErrorWithCode {
|
||||
if len(args) > 0 {
|
||||
return &ErrorWithCode{
|
||||
err: fmt.Errorf(format, args...),
|
||||
code: code,
|
||||
}
|
||||
} else {
|
||||
return &ErrorWithCode{
|
||||
code: code,
|
||||
err: nil,
|
||||
message: format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/rs/zerolog/log"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DistrictForReport(ctx context.Context, report_id string) (*models.Organization, error) {
|
||||
|
|
@ -56,14 +56,14 @@ func GenerateReportID() (string, error) {
|
|||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func RegisterNotificationEmail(ctx context.Context, txn bob.Executor, report_id string, destination string) *ErrorWithCode {
|
||||
func RegisterNotificationEmail(ctx context.Context, txn bob.Executor, report_id string, destination string) error {
|
||||
report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to find report")
|
||||
return fmt.Errorf("Failed to find report: %w", e)
|
||||
}
|
||||
e = email.EnsureInDB(ctx, destination)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to ensure phone is in DB")
|
||||
return fmt.Errorf("Failed to ensure phone is in DB: %w", e)
|
||||
}
|
||||
err := addNotificationEmail(ctx, txn, report, destination)
|
||||
if err != nil {
|
||||
|
|
@ -73,14 +73,14 @@ func RegisterNotificationEmail(ctx context.Context, txn bob.Executor, report_id
|
|||
return nil
|
||||
}
|
||||
|
||||
func RegisterNotificationPhone(ctx context.Context, txn bob.Executor, report_id string, phone types.E164) *ErrorWithCode {
|
||||
func RegisterNotificationPhone(ctx context.Context, txn bob.Executor, report_id string, phone types.E164) error {
|
||||
report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to find report")
|
||||
return fmt.Errorf("Failed to find report: %w", e)
|
||||
}
|
||||
e = text.EnsureInDB(ctx, db.PGInstance.BobDB, phone)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to ensure phone is in DB")
|
||||
return fmt.Errorf("Failed to ensure phone is in DB: %w", e)
|
||||
}
|
||||
err := addNotificationPhone(ctx, txn, report, phone)
|
||||
if err != nil {
|
||||
|
|
@ -90,10 +90,10 @@ func RegisterNotificationPhone(ctx context.Context, txn bob.Executor, report_id
|
|||
return nil
|
||||
}
|
||||
|
||||
func RegisterSubscriptionEmail(ctx context.Context, txn bob.Executor, destination string) *ErrorWithCode {
|
||||
func RegisterSubscriptionEmail(ctx context.Context, txn bob.Executor, destination string) error {
|
||||
e := email.EnsureInDB(ctx, destination)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to ensure email is in DB")
|
||||
return fmt.Errorf("Failed to ensure email is in DB: %w", e)
|
||||
}
|
||||
setter := models.PublicreportSubscribeEmailSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
|
|
@ -103,16 +103,15 @@ func RegisterSubscriptionEmail(ctx context.Context, txn bob.Executor, destinatio
|
|||
}
|
||||
_, err := models.PublicreportSubscribeEmails.Insert(&setter).Exec(ctx, txn)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save new subscription email row")
|
||||
return newInternalError(err, "Failed to save new subscription email row")
|
||||
return fmt.Errorf("Failed to save new subscription email row: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func RegisterSubscriptionPhone(ctx context.Context, txn bob.Executor, phone types.E164) *ErrorWithCode {
|
||||
func RegisterSubscriptionPhone(ctx context.Context, txn bob.Executor, phone types.E164) error {
|
||||
e := text.EnsureInDB(ctx, db.PGInstance.BobDB, phone)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to ensure phone is in DB")
|
||||
return fmt.Errorf("Failed to ensure phone is in DB: %w", e)
|
||||
}
|
||||
setter := models.PublicreportSubscribePhoneSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
|
|
@ -122,16 +121,15 @@ func RegisterSubscriptionPhone(ctx context.Context, txn bob.Executor, phone type
|
|||
}
|
||||
_, err := models.PublicreportSubscribePhones.Insert(&setter).Exec(ctx, txn)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save new subscription phone row")
|
||||
return newInternalError(err, "Failed to save new subscription phone row")
|
||||
return fmt.Errorf("Failed to save new subscription phone row: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveReporter(ctx context.Context, txn bob.Executor, report_id string, name string, email string, phone *types.E164, has_consent bool) *ErrorWithCode {
|
||||
func SaveReporter(ctx context.Context, txn bob.Executor, report_id string, name string, email string, phone *types.E164, has_consent bool) error {
|
||||
report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id)
|
||||
if e != nil {
|
||||
return newInternalError(e, "Failed to find report")
|
||||
return fmt.Errorf("Failed to find report: %w", e)
|
||||
}
|
||||
if name != "" {
|
||||
err := updateReporterName(ctx, txn, report, name)
|
||||
|
|
@ -162,7 +160,7 @@ func reportByPublicID(ctx context.Context, txn bob.Executor, public_id string) (
|
|||
models.SelectWhere.PublicreportReports.PublicID.EQ(public_id),
|
||||
).One(ctx, txn)
|
||||
}
|
||||
func addNotificationEmail(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, email string) *ErrorWithCode {
|
||||
func addNotificationEmail(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, email string) error {
|
||||
setter := models.PublicreportNotifyEmailSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
Deleted: omitnull.FromPtr[time.Time](nil),
|
||||
|
|
@ -171,11 +169,11 @@ func addNotificationEmail(ctx context.Context, txn bob.Executor, report *models.
|
|||
}
|
||||
_, err := models.PublicreportNotifyEmails.Insert(&setter).Exec(ctx, txn)
|
||||
if err != nil {
|
||||
return newInternalError(err, "Failed to save new notification email row")
|
||||
return fmt.Errorf("Failed to save new notification email row: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func addNotificationPhone(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, phone types.E164) *ErrorWithCode {
|
||||
func addNotificationPhone(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, phone types.E164) error {
|
||||
var err error
|
||||
setter := models.PublicreportNotifyPhoneSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
|
|
@ -185,39 +183,38 @@ func addNotificationPhone(ctx context.Context, txn bob.Executor, report *models.
|
|||
}
|
||||
_, err = models.PublicreportNotifyPhones.Insert(&setter).Exec(ctx, txn)
|
||||
if err != nil {
|
||||
return newInternalError(err, "Failed to save new notification phone row")
|
||||
return fmt.Errorf("Failed to save new notification phone row: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func updateReporterConsent(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, has_consent bool) *ErrorWithCode {
|
||||
func updateReporterConsent(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, has_consent bool) error {
|
||||
return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{
|
||||
ReporterContactConsent: omitnull.From(has_consent),
|
||||
})
|
||||
}
|
||||
func updateReporterEmail(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, email string) *ErrorWithCode {
|
||||
func updateReporterEmail(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, email string) error {
|
||||
return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{
|
||||
ReporterEmail: omit.From(email),
|
||||
})
|
||||
}
|
||||
func updateReporterName(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, name string) *ErrorWithCode {
|
||||
func updateReporterName(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, name string) error {
|
||||
return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{
|
||||
ReporterName: omit.From(name),
|
||||
})
|
||||
}
|
||||
func updateReportCol(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, setter *models.PublicreportReportSetter) *ErrorWithCode {
|
||||
func updateReportCol(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, setter *models.PublicreportReportSetter) error {
|
||||
err := report.Update(ctx, txn, setter)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("public_id", report.PublicID).Int32("report_id", report.ID).Msg("Failed to update report")
|
||||
return newInternalError(err, "Failed to update nuisance report in the database")
|
||||
return fmt.Errorf("Failed to update nuisance report in the database: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func updateReporterPhone(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, phone types.E164) *ErrorWithCode {
|
||||
func updateReporterPhone(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, phone types.E164) error {
|
||||
err := report.Update(ctx, txn, &models.PublicreportReportSetter{
|
||||
ReporterPhone: omit.From(phone.PhoneString()),
|
||||
})
|
||||
if err != nil {
|
||||
return newInternalError(err, "Failed to update report: %w", err)
|
||||
return fmt.Errorf("Failed to update report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ import (
|
|||
)
|
||||
|
||||
type SomeReport interface {
|
||||
addNotificationEmail(context.Context, bob.Executor, string) *ErrorWithCode
|
||||
addNotificationPhone(context.Context, bob.Executor, types.E164) *ErrorWithCode
|
||||
addNotificationEmail(context.Context, bob.Executor, string) error
|
||||
addNotificationPhone(context.Context, bob.Executor, types.E164) error
|
||||
districtID(context.Context) *int32
|
||||
updateReporterConsent(context.Context, bob.Executor, bool) *ErrorWithCode
|
||||
updateReporterEmail(context.Context, bob.Executor, string) *ErrorWithCode
|
||||
updateReporterName(context.Context, bob.Executor, string) *ErrorWithCode
|
||||
updateReporterPhone(context.Context, bob.Executor, types.E164) *ErrorWithCode
|
||||
updateReporterConsent(context.Context, bob.Executor, bool) error
|
||||
updateReporterEmail(context.Context, bob.Executor, string) error
|
||||
updateReporterName(context.Context, bob.Executor, string) error
|
||||
updateReporterPhone(context.Context, bob.Executor, types.E164) error
|
||||
PublicReportID() string
|
||||
reportID() int32
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/publicreport"
|
||||
pr "github.com/Gleipnir-Technology/nidus-sync/platform/publicreport"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
|
|
@ -48,7 +48,7 @@ func toImageURLs(m map[string][]uuid.UUID, id string) []string {
|
|||
return urls
|
||||
}
|
||||
func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*communicationList, *nhttp.ErrorWithStatus) {
|
||||
reports, err := publicreport.ReportsForOrganization(ctx, user.Organization.ID)
|
||||
reports, err := pr.ReportsForOrganization(ctx, user.Organization.ID)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("nuisance report query: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ package resource
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
|
@ -17,6 +21,7 @@ type district struct {
|
|||
PhoneOffice string `json:"phone_office"`
|
||||
Slug string `json:"slug"`
|
||||
URLLogo string `json:"url_logo"`
|
||||
URLWebsite string `json:"url_website"`
|
||||
}
|
||||
|
||||
func District(r *router) *districtR {
|
||||
|
|
@ -25,6 +30,23 @@ func District(r *router) *districtR {
|
|||
}
|
||||
}
|
||||
|
||||
func (res *districtR) GetByID(ctx context.Context, r *http.Request, query QueryParams) (*district, *nhttp.ErrorWithStatus) {
|
||||
vars := mux.Vars(r)
|
||||
id_str := vars["id"]
|
||||
id, err := strconv.Atoi(id_str)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewBadRequest("id conversion: %w", err)
|
||||
}
|
||||
org, err := platform.OrganizationByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("get org: %w", err)
|
||||
}
|
||||
district, err := newDistrict(res.router, org)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("new district: %w", err)
|
||||
}
|
||||
return district, nil
|
||||
}
|
||||
func (res *districtR) List(ctx context.Context, r *http.Request, query QueryParams) ([]*district, *nhttp.ErrorWithStatus) {
|
||||
organizations, err := platform.OrganizationList(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -32,20 +54,32 @@ func (res *districtR) List(ctx context.Context, r *http.Request, query QueryPara
|
|||
}
|
||||
districts := make([]*district, 0)
|
||||
for _, org := range organizations {
|
||||
slug := org.Slug()
|
||||
if slug == "" {
|
||||
district, err := newDistrict(res.router, org)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("make district: %w", err)
|
||||
}
|
||||
if district == nil {
|
||||
continue
|
||||
}
|
||||
logo, err := res.router.SlugToURI("district.logo.BySlug", slug)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("logo url: %w", err)
|
||||
}
|
||||
districts = append(districts, &district{
|
||||
Name: org.Name(),
|
||||
PhoneOffice: org.PhoneOffice(),
|
||||
Slug: slug,
|
||||
URLLogo: logo,
|
||||
})
|
||||
districts = append(districts, district)
|
||||
}
|
||||
return districts, nil
|
||||
}
|
||||
|
||||
func newDistrict(r *router, org *platform.Organization) (*district, error) {
|
||||
slug := org.Slug()
|
||||
if slug == "" {
|
||||
return nil, nil
|
||||
}
|
||||
logo, err := r.SlugToURI("district.logo.BySlug", slug)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("logo url: %w", err)
|
||||
}
|
||||
return &district{
|
||||
Name: org.Name(),
|
||||
PhoneOffice: org.PhoneOffice(),
|
||||
Slug: slug,
|
||||
URLLogo: logo,
|
||||
URLWebsite: org.Website(),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ type nuisanceR struct {
|
|||
router *router
|
||||
}
|
||||
type nuisance struct {
|
||||
ID string `json:"id"`
|
||||
District string `json:"district"`
|
||||
ID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
type nuisanceForm struct {
|
||||
AdditionalInfo string `schema:"additional-info"`
|
||||
|
|
@ -181,7 +183,17 @@ func (res *nuisanceR) Create(ctx context.Context, r *http.Request, n nuisanceFor
|
|||
if err != nil {
|
||||
return nil, nhttp.NewError("create nuisance report: %w", err)
|
||||
}
|
||||
uri, err := res.router.IDStrToURI("publicreport.ByIDGet", report.PublicID)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("generate uri: %w", err)
|
||||
}
|
||||
district_uri, err := res.router.IDToURI("district.ByIDGet", int(report.OrganizationID))
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("generate district uri: %w", err)
|
||||
}
|
||||
return &nuisance{
|
||||
ID: report.PublicID,
|
||||
District: district_uri,
|
||||
ID: report.PublicID,
|
||||
URI: uri,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
46
resource/publicreport.go
Normal file
46
resource/publicreport.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"net/http"
|
||||
//"github.com/rs/zerolog/log"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type publicreportR struct {
|
||||
router *router
|
||||
}
|
||||
|
||||
type publicreport struct {
|
||||
ID string `json:"id"`
|
||||
District string `json:"district"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
func Publicreport(r *router) *publicreportR {
|
||||
return &publicreportR{
|
||||
router: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (res *publicreportR) ByID(ctx context.Context, r *http.Request, query QueryParams) (*publicreport, *nhttp.ErrorWithStatus) {
|
||||
vars := mux.Vars(r)
|
||||
public_id := vars["id"]
|
||||
if public_id == "" {
|
||||
return nil, nhttp.NewBadRequest("You must provid an ID")
|
||||
}
|
||||
report, err := platform.PublicreportByID(ctx, public_id)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("get report: %w", err)
|
||||
}
|
||||
district_uri, err := res.router.IDToURI("district.ByIDGet", int(report.OrganizationID))
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("district uri: %w", err)
|
||||
}
|
||||
return &publicreport{
|
||||
District: district_uri,
|
||||
ID: report.PublicID,
|
||||
}, nil
|
||||
}
|
||||
56
resource/publicreport_notification.go
Normal file
56
resource/publicreport_notification.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type publicreportNotificationR struct {
|
||||
router *router
|
||||
}
|
||||
|
||||
type publicreportNotification struct {
|
||||
Consent bool `json:"consent"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Notification bool `json:"notification"`
|
||||
Phone string `json:"phone"`
|
||||
ReportID string `json:"report_id"`
|
||||
Subscription bool `json:"subscription"`
|
||||
}
|
||||
|
||||
func PublicreportNotification(r *router) *publicreportNotificationR {
|
||||
return &publicreportNotificationR{
|
||||
router: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (res *publicreportNotificationR) Create(ctx context.Context, r *http.Request, n publicreportNotification) (*publicreportNotification, *nhttp.ErrorWithStatus) {
|
||||
var err error
|
||||
var phone *types.E164
|
||||
if n.Phone != "" {
|
||||
phone, err = text.ParsePhoneNumber(n.Phone)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewBadRequest("can't parse phone: %w", err)
|
||||
}
|
||||
}
|
||||
err = platform.PublicreportNotificationCreate(ctx, platform.PublicreportNotification{
|
||||
Consent: n.Consent,
|
||||
Email: n.Email,
|
||||
Name: n.Name,
|
||||
Notification: n.Notification,
|
||||
Phone: phone,
|
||||
ReportID: n.ReportID,
|
||||
Subscription: n.Subscription,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("create notification: %w", err)
|
||||
}
|
||||
log.Info().Str("name", n.Name).Str("email", n.Email).Str("phone", n.Phone).Str("report_id", n.ReportID).Msg("Added reporter data")
|
||||
return &n, nil
|
||||
}
|
||||
|
|
@ -48,11 +48,14 @@ func (r *router) UUIDFromURI(route string, uri string) (*uuid.UUID, error) {
|
|||
}
|
||||
func (r *router) IDToURI(route string, id int) (string, error) {
|
||||
i := strconv.FormatInt(int64(id), 10)
|
||||
return r.IDStrToURI(route, i)
|
||||
}
|
||||
func (r *router) IDStrToURI(route string, id string) (string, error) {
|
||||
handler := r.router.Get(route)
|
||||
if handler == nil {
|
||||
return "", fmt.Errorf("nil handler '%s'", route)
|
||||
}
|
||||
uri, err := handler.URL("id", i)
|
||||
uri, err := handler.URL("id", id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build uri: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,98 +1 @@
|
|||
package rmo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func postRegisterNotifications(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
has_consent := boolFromForm(r, "consent")
|
||||
has_notification := boolFromForm(r, "notification")
|
||||
has_subscribe := boolFromForm(r, "subscribe")
|
||||
email := r.PostFormValue("email")
|
||||
name := r.PostFormValue("name")
|
||||
phone_str := r.PostFormValue("phone")
|
||||
report_id := r.PostFormValue("report_id")
|
||||
|
||||
var phone *types.E164
|
||||
if phone_str != "" {
|
||||
phone, err = text.ParsePhoneNumber(phone_str)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=invalid-phone&report=%s", report_id), http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to begin transaction")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=transaction-failed&report=%s", report_id), http.StatusFound)
|
||||
return
|
||||
}
|
||||
defer txn.Rollback(ctx)
|
||||
rep, err := models.PublicreportReports.Query(
|
||||
models.SelectWhere.PublicreportReports.PublicID.EQ(report_id),
|
||||
).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get report")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=report-location-failed&report=%s", report_id), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
e := report.SaveReporter(ctx, txn, report_id, name, email, phone, has_consent)
|
||||
if e != nil {
|
||||
log.Error().Err(e).Str("name", name).Msg("Failed to save reporter")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=%s&report=%s", e.Code(), report_id), http.StatusFound)
|
||||
return
|
||||
}
|
||||
if email != "" {
|
||||
if has_subscribe {
|
||||
e := report.RegisterSubscriptionEmail(ctx, txn, email)
|
||||
if e != nil {
|
||||
log.Error().Err(e).Str("email", email).Msg("Failed to register subscription email")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=%s&report=%s", e.Code(), report_id), http.StatusFound)
|
||||
}
|
||||
}
|
||||
if has_notification {
|
||||
e := report.RegisterNotificationEmail(ctx, txn, report_id, email)
|
||||
if e != nil {
|
||||
log.Error().Err(e).Str("email", email).Msg("Failed to register notification email")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=%s&report=%s", e.Code(), report_id), http.StatusFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
if phone != nil {
|
||||
if has_subscribe {
|
||||
e := report.RegisterSubscriptionPhone(ctx, txn, *phone)
|
||||
if e != nil {
|
||||
log.Error().Err(e).Str("phone", phone_str).Msg("Failed to register subscription phone")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=%s&report=%s", e.Code(), report_id), http.StatusFound)
|
||||
}
|
||||
}
|
||||
if has_notification {
|
||||
e := report.RegisterNotificationPhone(ctx, txn, report_id, *phone)
|
||||
if e != nil {
|
||||
log.Error().Err(e).Str("phone", phone_str).Msg("Failed to register notification phone")
|
||||
http.Redirect(w, r, fmt.Sprintf("/error?code=%s&report=%s", e.Code(), report_id), http.StatusFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
txn.Commit(ctx)
|
||||
platform.PublicReportReporterUpdated(ctx, rep.OrganizationID, report_id)
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/register-notifications-complete?report=%s", report_id), http.StatusFound)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,5 +52,5 @@ func Router(r *mux.Router) {
|
|||
r.HandleFunc("/terms-of-service", getTerms).Methods("GET")
|
||||
*/
|
||||
static.AddStaticRoute(r, "/static")
|
||||
r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/rmo"))
|
||||
r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/rmo")).Methods("GET")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ func fileFromFilesystem(path string) (*http.File, error) {
|
|||
// Try to open from local filesystem for development
|
||||
fileToServe, err = localFS.Open(path)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("path", path).Msg("Failed to read static file for dev")
|
||||
//log.Warn().Err(err).Str("path", path).Msg("Failed to read static file for dev")
|
||||
found = false
|
||||
} else {
|
||||
found = true
|
||||
|
|
|
|||
|
|
@ -39,5 +39,5 @@ func Router(r *mux.Router) {
|
|||
//r.HandleFunc("/_/*", getRoot)
|
||||
|
||||
static.AddStaticRoute(r, "/static")
|
||||
r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/sync"))
|
||||
r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/sync")).Methods("GET")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
import { onMounted, ref } from "vue";
|
||||
//import { useHead } from "@vueuse/head";
|
||||
import { router } from "@/rmo/router";
|
||||
import { useDistrictStore } from "@/rmo/store/district";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import type { District } from "@/type/api";
|
||||
|
||||
const district = useDistrictStore();
|
||||
const district = useStoreDistrict();
|
||||
const count = ref<number>(0);
|
||||
const message = ref<string>("hey");
|
||||
|
||||
|
|
@ -21,11 +21,11 @@ const increment = (): void => {
|
|||
|
||||
onMounted(() => {
|
||||
district
|
||||
.get()
|
||||
.list()
|
||||
.then((districts: District[]) => {
|
||||
console.log("got districts");
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch((e: Error) => {
|
||||
console.error("Failed to get districts", e);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -535,8 +535,15 @@ import ImageUpload, { Image } from "@/components/ImageUpload.vue";
|
|||
import MapLocator from "@/components/MapLocator.vue";
|
||||
import { useGeocodeStore } from "@/store/geocode";
|
||||
import { useLocationStore } from "@/store/location";
|
||||
import { useStorePublicreport } from "@/store/publicreport";
|
||||
import type { Marker } from "@/types";
|
||||
import type { Address, Geocode, GeocodeSuggestion, Location } from "@/type/api";
|
||||
import type {
|
||||
Address,
|
||||
Geocode,
|
||||
GeocodeSuggestion,
|
||||
Location,
|
||||
Publicreport,
|
||||
} from "@/type/api";
|
||||
import type { Camera } from "@/type/map";
|
||||
|
||||
const address = ref<string>("");
|
||||
|
|
@ -551,6 +558,7 @@ const marker = ref<Marker | null>(null);
|
|||
const showMore = ref<boolean>(false);
|
||||
const selectedSuggestion = ref<GeocodeSuggestion | null>(null);
|
||||
const locationStore = useLocationStore();
|
||||
const storePublicreport = useStorePublicreport();
|
||||
const geocode = useGeocodeStore();
|
||||
const markers = computed((): Marker[] => {
|
||||
if (marker.value) {
|
||||
|
|
@ -641,8 +649,9 @@ async function doSubmit() {
|
|||
body: formData,
|
||||
// Don't set Content-Type, the borwser should do it
|
||||
});
|
||||
const data = await resp.json();
|
||||
router.push("/complete/" + data.id);
|
||||
const data: Publicreport = (await resp.json()) as Publicreport;
|
||||
storePublicreport.add(data);
|
||||
router.push("/submitted/" + data.id);
|
||||
} catch (error) {
|
||||
errorMessage.value =
|
||||
error instanceof Error ? error.message : "Upload failed";
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import HomeBase from "@/rmo/view/Home.vue";
|
|||
import HomeDistrict from "@/rmo/view/district/Home.vue";
|
||||
import NuisanceBase from "@/rmo/view/Nuisance.vue";
|
||||
import NuisanceDistrict from "@/rmo/view/district/Nuisance.vue";
|
||||
import ReportSubmitted from "@/rmo/view/ReportSubmitted.vue";
|
||||
import StatusBase from "@/rmo/view/Status.vue";
|
||||
import StatusDistrict from "@/rmo/view/district/Status.vue";
|
||||
import Water from "@/rmo/view/Water.vue";
|
||||
|
|
@ -106,6 +107,12 @@ const routes: RouteRecordRaw[] = [
|
|||
component: WaterDistrict,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/submitted/:id",
|
||||
name: "ReportSubmitted",
|
||||
component: ReportSubmitted,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/status",
|
||||
name: "StatusBase",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,39 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { District } from "@/type/api";
|
||||
import { ref } from "vue";
|
||||
import type { District } from "@/type/api";
|
||||
|
||||
export const useDistrictStore = defineStore("district", () => {
|
||||
const districts = ref<District[] | null>(null);
|
||||
export const useStoreDistrict = defineStore("district", () => {
|
||||
// State
|
||||
const _byURI = ref<Map<string, District>>(new Map());
|
||||
const error = ref<string | null>(null);
|
||||
const loading = ref<boolean>(false);
|
||||
const ongoingFetch = ref<Promise<District[]> | null>(null);
|
||||
|
||||
// Actions
|
||||
async function byURI(uri: string): Promise<District | undefined> {
|
||||
let district = _byURI.value.get(uri);
|
||||
if (district) {
|
||||
return district;
|
||||
}
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await fetch(uri);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const body = await response.json();
|
||||
_byURI.value.set(uri, body);
|
||||
return body;
|
||||
} catch (e) {
|
||||
console.error("Error loading users:", e);
|
||||
error.value = e instanceof Error ? e.message : "an error ocurred";
|
||||
throw e;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
async function fetchDistricts(): Promise<District[]> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
|
@ -17,7 +43,9 @@ export const useDistrictStore = defineStore("district", () => {
|
|||
if (!response.ok) throw new Error("Failed to fetch districts");
|
||||
|
||||
const data: District[] = await response.json();
|
||||
districts.value = data;
|
||||
data.forEach((d: District) => {
|
||||
_byURI.value.set(d.uri, d);
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "an error ocurred";
|
||||
|
|
@ -27,9 +55,9 @@ export const useDistrictStore = defineStore("district", () => {
|
|||
loading.value = false;
|
||||
}
|
||||
}
|
||||
async function get(): Promise<District[]> {
|
||||
if (districts.value != null) {
|
||||
return districts.value;
|
||||
async function list(): Promise<District[]> {
|
||||
if (_byURI.value.size > 0) {
|
||||
return Array.from(_byURI.value.values());
|
||||
}
|
||||
|
||||
if (ongoingFetch.value !== null) {
|
||||
|
|
@ -37,12 +65,13 @@ export const useDistrictStore = defineStore("district", () => {
|
|||
}
|
||||
|
||||
const s = await fetchDistricts();
|
||||
districts.value = s;
|
||||
ongoingFetch.value = null;
|
||||
return s;
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
get,
|
||||
// Actions
|
||||
byURI,
|
||||
list,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,18 +30,18 @@ body > .container-fluid {
|
|||
import { computed, onMounted, ref } from "vue";
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
|
||||
import { useDistrictStore } from "@/rmo/store/district";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import Intro from "@/rmo/content/compliance/Intro.vue";
|
||||
import type { District } from "@/type/api";
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const districtStore = useDistrictStore();
|
||||
const districtStore = useStoreDistrict();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const district = computedAsync(async (): Promise<District | undefined> => {
|
||||
const districts = await districtStore.get();
|
||||
const districts = await districtStore.list();
|
||||
return districts.find((district: District) => district.slug == props.slug);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
326
ts/rmo/view/ReportSubmitted.vue
Normal file
326
ts/rmo/view/ReportSubmitted.vue
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<style scoped>
|
||||
.logo {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<!-- Success Card -->
|
||||
<div class="card shadow-sm border-success mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h3 class="my-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
class="bi bi-check-circle-fill me-2"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"
|
||||
/>
|
||||
</svg>
|
||||
Report Successfully Submitted
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<div class="alert alert-info py-3">
|
||||
<strong>Your Report ID:</strong>
|
||||
<span class="fs-4 fw-bold">{{ id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<div>
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-person-rolodex"></i>
|
||||
Follow-up
|
||||
</h4>
|
||||
<p>
|
||||
Please provide your contact information in case the district has
|
||||
any follow-up questions.
|
||||
</p>
|
||||
<form
|
||||
id="notificationForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
class="needs-validation"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="name">Name</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-person-lines-fill"></i>
|
||||
</span>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="name"
|
||||
maxlength="100"
|
||||
placeholder="Adam Smith"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Address</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-envelope"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
maxlength="200"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">Phone Number</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-phone"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M11 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6zM5 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H5z"
|
||||
/>
|
||||
<path d="M8 14a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
class="form-control"
|
||||
id="phone"
|
||||
maxlength="100"
|
||||
placeholder="(123) 456-7890"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 form-check">
|
||||
<input
|
||||
v-model="formData.consent"
|
||||
class="form-check-input"
|
||||
id="consent"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="consent">
|
||||
I consent to being contacted at my email address or phone
|
||||
number above just for the purposes of this report.
|
||||
<i
|
||||
class="bi bi-info-circle-fill text-primary ms-1"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="We will never sell your information. We'll send you notifications, but only if you ask us to. We'll share your information with the district that is in change of mosquito control in the area you've reported, but not with anybody else."
|
||||
></i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 form-check">
|
||||
<input
|
||||
v-model="formData.notification"
|
||||
class="form-check-input"
|
||||
id="notification"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="notification">
|
||||
I'd like to get updates about my report as it gets handled.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 form-check">
|
||||
<input
|
||||
v-model="formData.subscribe"
|
||||
class="form-check-input"
|
||||
id="subscribe"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="subscribe">
|
||||
<template v-if="!district">
|
||||
I'd like to subscribe to periodic updates from my mosquito
|
||||
control district.
|
||||
</template>
|
||||
<template v-else>
|
||||
I'd like to subscribe to periodic updates from
|
||||
{{ district.name }}
|
||||
</template>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Update report
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<!-- Status Check Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="currentColor"
|
||||
class="bi bi-search me-2"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
|
||||
/>
|
||||
</svg>
|
||||
Check Your Report Status
|
||||
</h4>
|
||||
<p>
|
||||
You can check the status of your report at any time using your
|
||||
Report ID.
|
||||
</p>
|
||||
<a :href="`/status/${id}`" class="btn btn-outline-primary">
|
||||
Check Status
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div v-if="district" class="mb-4 text-center">
|
||||
<p>Your report will be handled by</p>
|
||||
<p>
|
||||
<b>{{ district.name }}</b>
|
||||
</p>
|
||||
<a :href="district.url_website">
|
||||
<img
|
||||
class="logo"
|
||||
:src="district.url_logo"
|
||||
:alt="district.name + 'logo'"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="text-center">
|
||||
<RouterLink to="/nuisance" class="btn btn-outline-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-plus-circle me-1"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"
|
||||
/>
|
||||
<path
|
||||
d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"
|
||||
/>
|
||||
</svg>
|
||||
Submit Another Report
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import { useStorePublicreport } from "@/store/publicreport";
|
||||
import type { District, Publicreport } from "@/type/api";
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
consent: boolean;
|
||||
notification: boolean;
|
||||
subscribe: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const formData = ref<FormData>({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
consent: true,
|
||||
notification: false,
|
||||
subscribe: false,
|
||||
});
|
||||
const router = useRouter();
|
||||
const storeDistrict = useStoreDistrict();
|
||||
const storePublicreport = useStorePublicreport();
|
||||
|
||||
const report = computedAsync(async (): Promise<Publicreport | undefined> => {
|
||||
return await storePublicreport.byID(props.id);
|
||||
});
|
||||
const district = computedAsync(async (): Promise<District | undefined> => {
|
||||
if (!(report.value && report.value.district)) {
|
||||
return undefined;
|
||||
}
|
||||
return await storeDistrict.byURI(report.value.district);
|
||||
});
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/publicreport-notification", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
report_id: props.id,
|
||||
...formData.value,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Handle success (e.g., show a success message)
|
||||
console.log("Form submitted successfully");
|
||||
} else {
|
||||
// Handle error
|
||||
console.error("Form submission failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
|
@ -46,17 +46,17 @@ import { ref } from "vue";
|
|||
import { computedAsync } from "@vueuse/core";
|
||||
import Home from "@/rmo/content/Home.vue";
|
||||
import type { District } from "@/type/api";
|
||||
import { useDistrictStore } from "@/rmo/store/district";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import HeaderDistrict from "@/components/HeaderDistrict.vue";
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const districtStore = useDistrictStore();
|
||||
const districtStore = useStoreDistrict();
|
||||
|
||||
const district = computedAsync(async (): Promise<District | undefined> => {
|
||||
const districts = await districtStore.get();
|
||||
const districts = await districtStore.list();
|
||||
return districts.find((district: District) => district.slug == props.slug);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ import { ref } from "vue";
|
|||
import { computedAsync } from "@vueuse/core";
|
||||
import Nuisance from "@/rmo/content/Nuisance.vue";
|
||||
import type { District } from "@/type/api";
|
||||
import { useDistrictStore } from "@/rmo/store/district";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import HeaderDistrict from "@/components/HeaderDistrict.vue";
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const districtStore = useDistrictStore();
|
||||
const districtStore = useStoreDistrict();
|
||||
|
||||
const district = computedAsync(async (): Promise<District | undefined> => {
|
||||
const districts = await districtStore.get();
|
||||
const districts = await districtStore.list();
|
||||
return districts.find((district: District) => district.slug == props.slug);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ import { ref } from "vue";
|
|||
import { computedAsync } from "@vueuse/core";
|
||||
import Status from "@/rmo/content/Status.vue";
|
||||
import type { District } from "@/type/api";
|
||||
import { useDistrictStore } from "@/rmo/store/district";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import HeaderDistrict from "@/components/HeaderDistrict.vue";
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const districtStore = useDistrictStore();
|
||||
const storeDistrict = useStoreDistrict();
|
||||
|
||||
const district = computedAsync(async (): Promise<District | undefined> => {
|
||||
const districts = await districtStore.get();
|
||||
const districts = await storeDistrict.list();
|
||||
return districts.find((district: District) => district.slug == props.slug);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ import { ref } from "vue";
|
|||
import { computedAsync } from "@vueuse/core";
|
||||
import Water from "@/rmo/content/Water.vue";
|
||||
import type { District } from "@/type/api";
|
||||
import { useDistrictStore } from "@/rmo/store/district";
|
||||
import { useStoreDistrict } from "@/rmo/store/district";
|
||||
import HeaderDistrict from "@/components/HeaderDistrict.vue";
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const districtStore = useDistrictStore();
|
||||
const districtStore = useStoreDistrict();
|
||||
|
||||
const district = computedAsync(async (): Promise<District | undefined> => {
|
||||
const districts = await districtStore.get();
|
||||
const districts = await districtStore.list();
|
||||
return districts.find((district: District) => district.slug == props.slug);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
40
ts/store/publicreport.ts
Normal file
40
ts/store/publicreport.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import type { Publicreport } from "@/type/api";
|
||||
|
||||
export const useStorePublicreport = defineStore("publicreport", () => {
|
||||
// State
|
||||
const _byID = ref<Map<string, Publicreport>>(new Map());
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
//const ongoingFetch = ref<Promise<Publicreport[]> | null>(null);
|
||||
|
||||
function add(pr: Publicreport) {
|
||||
_byID.value.set(pr.id, pr);
|
||||
}
|
||||
// Actions
|
||||
async function byID(id: string): Promise<Publicreport | undefined> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const url = `/api/publicreport/${id}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const body = await response.json();
|
||||
_byID.value.set(id, body);
|
||||
return body;
|
||||
} catch (err) {
|
||||
console.error("Error loading users:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Actions
|
||||
add,
|
||||
byID,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { Signal } from "../types";
|
||||
import { SSEManager, type SSEMessage } from "../SSEManager";
|
||||
import { ref } from "vue";
|
||||
import { Signal } from "@/types";
|
||||
import { SSEManager, type SSEMessage } from "@/SSEManager";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
export const useSignalStore = defineStore("signal", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { Session, User } from "@/types";
|
||||
import { User } from "@/types";
|
||||
import { SSEManager, type SSEMessage } from "@/SSEManager";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export interface District {
|
|||
name: string;
|
||||
phone_office: string;
|
||||
slug: string;
|
||||
uri: string;
|
||||
url_logo: string;
|
||||
url_website: string;
|
||||
}
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
|
|
@ -30,3 +32,8 @@ export interface Geocode {
|
|||
cell: number;
|
||||
location: Location;
|
||||
}
|
||||
export interface Publicreport {
|
||||
district: string;
|
||||
id: string;
|
||||
uri: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue