Update nuisance submission to go to submitted page

This commit is contained in:
Eli Ribble 2026-04-08 17:49:32 +00:00
parent c41154a200
commit 2c0bfb9904
No known key found for this signature in database
31 changed files with 747 additions and 228 deletions

View file

@ -73,10 +73,15 @@ func AddRoutes(r *mux.Router) {
// Unauthenticated endpoints // Unauthenticated endpoints
district := resource.District(router) district := resource.District(router)
r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET") r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET")
r.Handle("/district/{id}", handlerJSON(district.GetByID)).Methods("GET").Name("district.ByIDGet")
geocode := resource.Geocode(router) geocode := resource.Geocode(router)
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET") r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST") r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST")
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET") 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", apiGetDistrict).Methods("GET")
r.HandleFunc("/district/{slug}/logo", apiGetDistrictLogo).Methods("GET").Name("district.logo.BySlug") r.HandleFunc("/district/{slug}/logo", apiGetDistrictLogo).Methods("GET").Name("district.logo.BySlug")

View file

@ -89,6 +89,9 @@ func (o Organization) ServiceRequestRecent(ctx context.Context) ([]*models.Field
func (o Organization) Slug() string { func (o Organization) Slug() string {
return o.model.Slug.GetOr("") return o.model.Slug.GetOr("")
} }
func (o Organization) Website() string {
return o.model.Website.GetOr("")
}
func OrganizationByID(ctx context.Context, id int) (*Organization, error) { func OrganizationByID(ctx context.Context, id int) (*Organization, error) {
org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, int32(id)) org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, int32(id))
if err != nil { if err != nil {

View file

@ -24,6 +24,15 @@ import (
"github.com/rs/zerolog/log" "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 { func PublicreportInvalid(ctx context.Context, user User, report_id string) error {
report, err := reportFromID(ctx, user, report_id) report, err := reportFromID(ctx, user, report_id)
if err != nil { if err != nil {

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

View file

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

View file

@ -16,7 +16,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/aarondl/opt/omit" "github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull" "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) { func DistrictForReport(ctx context.Context, report_id string) (*models.Organization, error) {
@ -56,14 +56,14 @@ func GenerateReportID() (string, error) {
return builder.String(), nil 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) report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id)
if e != nil { if e != nil {
return newInternalError(e, "Failed to find report") return fmt.Errorf("Failed to find report: %w", e)
} }
e = email.EnsureInDB(ctx, destination) e = email.EnsureInDB(ctx, destination)
if e != nil { 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) err := addNotificationEmail(ctx, txn, report, destination)
if err != nil { if err != nil {
@ -73,14 +73,14 @@ func RegisterNotificationEmail(ctx context.Context, txn bob.Executor, report_id
return nil 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) report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id)
if e != nil { 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) e = text.EnsureInDB(ctx, db.PGInstance.BobDB, phone)
if e != nil { 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) err := addNotificationPhone(ctx, txn, report, phone)
if err != nil { if err != nil {
@ -90,10 +90,10 @@ func RegisterNotificationPhone(ctx context.Context, txn bob.Executor, report_id
return nil 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) e := email.EnsureInDB(ctx, destination)
if e != nil { 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{ setter := models.PublicreportSubscribeEmailSetter{
Created: omit.From(time.Now()), 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) _, err := models.PublicreportSubscribeEmails.Insert(&setter).Exec(ctx, txn)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to save new subscription email row") return fmt.Errorf("Failed to save new subscription email row: %w", err)
return newInternalError(err, "Failed to save new subscription email row")
} }
return nil 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) e := text.EnsureInDB(ctx, db.PGInstance.BobDB, phone)
if e != nil { 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{ setter := models.PublicreportSubscribePhoneSetter{
Created: omit.From(time.Now()), 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) _, err := models.PublicreportSubscribePhones.Insert(&setter).Exec(ctx, txn)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to save new subscription phone row") return fmt.Errorf("Failed to save new subscription phone row: %w", err)
return newInternalError(err, "Failed to save new subscription phone row")
} }
return nil 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) report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id)
if e != nil { if e != nil {
return newInternalError(e, "Failed to find report") return fmt.Errorf("Failed to find report: %w", e)
} }
if name != "" { if name != "" {
err := updateReporterName(ctx, txn, report, 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), models.SelectWhere.PublicreportReports.PublicID.EQ(public_id),
).One(ctx, txn) ).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{ setter := models.PublicreportNotifyEmailSetter{
Created: omit.From(time.Now()), Created: omit.From(time.Now()),
Deleted: omitnull.FromPtr[time.Time](nil), 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) _, err := models.PublicreportNotifyEmails.Insert(&setter).Exec(ctx, txn)
if err != nil { 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 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 var err error
setter := models.PublicreportNotifyPhoneSetter{ setter := models.PublicreportNotifyPhoneSetter{
Created: omit.From(time.Now()), 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) _, err = models.PublicreportNotifyPhones.Insert(&setter).Exec(ctx, txn)
if err != nil { 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 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{ return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{
ReporterContactConsent: omitnull.From(has_consent), 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{ return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{
ReporterEmail: omit.From(email), 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{ return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{
ReporterName: omit.From(name), 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) err := report.Update(ctx, txn, setter)
if err != nil { if err != nil {
log.Error().Err(err).Str("public_id", report.PublicID).Int32("report_id", report.ID).Msg("Failed to update report") return fmt.Errorf("Failed to update nuisance report in the database: %w", err)
return newInternalError(err, "Failed to update nuisance report in the database")
} }
return nil 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{ err := report.Update(ctx, txn, &models.PublicreportReportSetter{
ReporterPhone: omit.From(phone.PhoneString()), ReporterPhone: omit.From(phone.PhoneString()),
}) })
if err != nil { if err != nil {
return newInternalError(err, "Failed to update report: %w", err) return fmt.Errorf("Failed to update report: %w", err)
} }
return nil return nil
} }

View file

@ -25,13 +25,13 @@ import (
) )
type SomeReport interface { type SomeReport interface {
addNotificationEmail(context.Context, bob.Executor, string) *ErrorWithCode addNotificationEmail(context.Context, bob.Executor, string) error
addNotificationPhone(context.Context, bob.Executor, types.E164) *ErrorWithCode addNotificationPhone(context.Context, bob.Executor, types.E164) error
districtID(context.Context) *int32 districtID(context.Context) *int32
updateReporterConsent(context.Context, bob.Executor, bool) *ErrorWithCode updateReporterConsent(context.Context, bob.Executor, bool) error
updateReporterEmail(context.Context, bob.Executor, string) *ErrorWithCode updateReporterEmail(context.Context, bob.Executor, string) error
updateReporterName(context.Context, bob.Executor, string) *ErrorWithCode updateReporterName(context.Context, bob.Executor, string) error
updateReporterPhone(context.Context, bob.Executor, types.E164) *ErrorWithCode updateReporterPhone(context.Context, bob.Executor, types.E164) error
PublicReportID() string PublicReportID() string
reportID() int32 reportID() int32
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/config"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http" nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform" "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/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -48,7 +48,7 @@ func toImageURLs(m map[string][]uuid.UUID, id string) []string {
return urls return urls
} }
func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*communicationList, *nhttp.ErrorWithStatus) { 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 { if err != nil {
return nil, nhttp.NewError("nuisance report query: %w", err) return nil, nhttp.NewError("nuisance report query: %w", err)
} }

View file

@ -2,8 +2,12 @@ package resource
import ( import (
"context" "context"
"fmt"
"strconv"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http" nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/gorilla/mux"
"net/http" "net/http"
//"github.com/rs/zerolog/log" //"github.com/rs/zerolog/log"
) )
@ -17,6 +21,7 @@ type district struct {
PhoneOffice string `json:"phone_office"` PhoneOffice string `json:"phone_office"`
Slug string `json:"slug"` Slug string `json:"slug"`
URLLogo string `json:"url_logo"` URLLogo string `json:"url_logo"`
URLWebsite string `json:"url_website"`
} }
func District(r *router) *districtR { 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) { func (res *districtR) List(ctx context.Context, r *http.Request, query QueryParams) ([]*district, *nhttp.ErrorWithStatus) {
organizations, err := platform.OrganizationList(ctx) organizations, err := platform.OrganizationList(ctx)
if err != nil { if err != nil {
@ -32,20 +54,32 @@ func (res *districtR) List(ctx context.Context, r *http.Request, query QueryPara
} }
districts := make([]*district, 0) districts := make([]*district, 0)
for _, org := range organizations { for _, org := range organizations {
slug := org.Slug() district, err := newDistrict(res.router, org)
if slug == "" { if err != nil {
return nil, nhttp.NewError("make district: %w", err)
}
if district == nil {
continue continue
} }
logo, err := res.router.SlugToURI("district.logo.BySlug", slug) districts = append(districts, district)
if err != nil {
return nil, nhttp.NewError("logo url: %w", err)
} }
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(), Name: org.Name(),
PhoneOffice: org.PhoneOffice(), PhoneOffice: org.PhoneOffice(),
Slug: slug, Slug: slug,
URLLogo: logo, URLLogo: logo,
}) URLWebsite: org.Website(),
} }, nil
return districts, nil
} }

View file

@ -28,7 +28,9 @@ type nuisanceR struct {
router *router router *router
} }
type nuisance struct { type nuisance struct {
District string `json:"district"`
ID string `json:"id"` ID string `json:"id"`
URI string `json:"uri"`
} }
type nuisanceForm struct { type nuisanceForm struct {
AdditionalInfo string `schema:"additional-info"` AdditionalInfo string `schema:"additional-info"`
@ -181,7 +183,17 @@ func (res *nuisanceR) Create(ctx context.Context, r *http.Request, n nuisanceFor
if err != nil { if err != nil {
return nil, nhttp.NewError("create nuisance report: %w", err) 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{ return &nuisance{
District: district_uri,
ID: report.PublicID, ID: report.PublicID,
URI: uri,
}, nil }, nil
} }

46
resource/publicreport.go Normal file
View 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
}

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

View file

@ -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) { func (r *router) IDToURI(route string, id int) (string, error) {
i := strconv.FormatInt(int64(id), 10) 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) handler := r.router.Get(route)
if handler == nil { if handler == nil {
return "", fmt.Errorf("nil handler '%s'", route) return "", fmt.Errorf("nil handler '%s'", route)
} }
uri, err := handler.URL("id", i) uri, err := handler.URL("id", id)
if err != nil { if err != nil {
return "", fmt.Errorf("build uri: %w", err) return "", fmt.Errorf("build uri: %w", err)
} }

View file

@ -1,98 +1 @@
package rmo 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)
}

View file

@ -52,5 +52,5 @@ func Router(r *mux.Router) {
r.HandleFunc("/terms-of-service", getTerms).Methods("GET") r.HandleFunc("/terms-of-service", getTerms).Methods("GET")
*/ */
static.AddStaticRoute(r, "/static") static.AddStaticRoute(r, "/static")
r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/rmo")) r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/rmo")).Methods("GET")
} }

View file

@ -88,7 +88,7 @@ func fileFromFilesystem(path string) (*http.File, error) {
// Try to open from local filesystem for development // Try to open from local filesystem for development
fileToServe, err = localFS.Open(path) fileToServe, err = localFS.Open(path)
if err != nil { 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 found = false
} else { } else {
found = true found = true

View file

@ -39,5 +39,5 @@ func Router(r *mux.Router) {
//r.HandleFunc("/_/*", getRoot) //r.HandleFunc("/_/*", getRoot)
static.AddStaticRoute(r, "/static") static.AddStaticRoute(r, "/static")
r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/sync")) r.PathPrefix("/").Handler(static.SinglePageApp("static/gen/sync")).Methods("GET")
} }

View file

@ -8,10 +8,10 @@
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
//import { useHead } from "@vueuse/head"; //import { useHead } from "@vueuse/head";
import { router } from "@/rmo/router"; import { router } from "@/rmo/router";
import { useDistrictStore } from "@/rmo/store/district"; import { useStoreDistrict } from "@/rmo/store/district";
import type { District } from "@/type/api"; import type { District } from "@/type/api";
const district = useDistrictStore(); const district = useStoreDistrict();
const count = ref<number>(0); const count = ref<number>(0);
const message = ref<string>("hey"); const message = ref<string>("hey");
@ -21,11 +21,11 @@ const increment = (): void => {
onMounted(() => { onMounted(() => {
district district
.get() .list()
.then((districts: District[]) => { .then((districts: District[]) => {
console.log("got districts"); console.log("got districts");
}) })
.catch((e) => { .catch((e: Error) => {
console.error("Failed to get districts", e); console.error("Failed to get districts", e);
}); });
}); });

View file

@ -535,8 +535,15 @@ import ImageUpload, { Image } from "@/components/ImageUpload.vue";
import MapLocator from "@/components/MapLocator.vue"; import MapLocator from "@/components/MapLocator.vue";
import { useGeocodeStore } from "@/store/geocode"; import { useGeocodeStore } from "@/store/geocode";
import { useLocationStore } from "@/store/location"; import { useLocationStore } from "@/store/location";
import { useStorePublicreport } from "@/store/publicreport";
import type { Marker } from "@/types"; 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"; import type { Camera } from "@/type/map";
const address = ref<string>(""); const address = ref<string>("");
@ -551,6 +558,7 @@ const marker = ref<Marker | null>(null);
const showMore = ref<boolean>(false); const showMore = ref<boolean>(false);
const selectedSuggestion = ref<GeocodeSuggestion | null>(null); const selectedSuggestion = ref<GeocodeSuggestion | null>(null);
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const storePublicreport = useStorePublicreport();
const geocode = useGeocodeStore(); const geocode = useGeocodeStore();
const markers = computed((): Marker[] => { const markers = computed((): Marker[] => {
if (marker.value) { if (marker.value) {
@ -641,8 +649,9 @@ async function doSubmit() {
body: formData, body: formData,
// Don't set Content-Type, the borwser should do it // Don't set Content-Type, the borwser should do it
}); });
const data = await resp.json(); const data: Publicreport = (await resp.json()) as Publicreport;
router.push("/complete/" + data.id); storePublicreport.add(data);
router.push("/submitted/" + data.id);
} catch (error) { } catch (error) {
errorMessage.value = errorMessage.value =
error instanceof Error ? error.message : "Upload failed"; error instanceof Error ? error.message : "Upload failed";

View file

@ -14,6 +14,7 @@ import HomeBase from "@/rmo/view/Home.vue";
import HomeDistrict from "@/rmo/view/district/Home.vue"; import HomeDistrict from "@/rmo/view/district/Home.vue";
import NuisanceBase from "@/rmo/view/Nuisance.vue"; import NuisanceBase from "@/rmo/view/Nuisance.vue";
import NuisanceDistrict from "@/rmo/view/district/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 StatusBase from "@/rmo/view/Status.vue";
import StatusDistrict from "@/rmo/view/district/Status.vue"; import StatusDistrict from "@/rmo/view/district/Status.vue";
import Water from "@/rmo/view/Water.vue"; import Water from "@/rmo/view/Water.vue";
@ -106,6 +107,12 @@ const routes: RouteRecordRaw[] = [
component: WaterDistrict, component: WaterDistrict,
props: true, props: true,
}, },
{
path: "/submitted/:id",
name: "ReportSubmitted",
component: ReportSubmitted,
props: true,
},
{ {
path: "/status", path: "/status",
name: "StatusBase", name: "StatusBase",

View file

@ -1,13 +1,39 @@
import { ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { District } from "@/type/api"; import { ref } from "vue";
import type { District } from "@/type/api";
export const useDistrictStore = defineStore("district", () => { export const useStoreDistrict = defineStore("district", () => {
const districts = ref<District[] | null>(null); // State
const _byURI = ref<Map<string, District>>(new Map());
const error = ref<string | null>(null); const error = ref<string | null>(null);
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const ongoingFetch = ref<Promise<District[]> | null>(null); 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[]> { async function fetchDistricts(): Promise<District[]> {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
@ -17,7 +43,9 @@ export const useDistrictStore = defineStore("district", () => {
if (!response.ok) throw new Error("Failed to fetch districts"); if (!response.ok) throw new Error("Failed to fetch districts");
const data: District[] = await response.json(); const data: District[] = await response.json();
districts.value = data; data.forEach((d: District) => {
_byURI.value.set(d.uri, d);
});
return data; return data;
} catch (e) { } catch (e) {
error.value = e instanceof Error ? e.message : "an error ocurred"; error.value = e instanceof Error ? e.message : "an error ocurred";
@ -27,9 +55,9 @@ export const useDistrictStore = defineStore("district", () => {
loading.value = false; loading.value = false;
} }
} }
async function get(): Promise<District[]> { async function list(): Promise<District[]> {
if (districts.value != null) { if (_byURI.value.size > 0) {
return districts.value; return Array.from(_byURI.value.values());
} }
if (ongoingFetch.value !== null) { if (ongoingFetch.value !== null) {
@ -37,12 +65,13 @@ export const useDistrictStore = defineStore("district", () => {
} }
const s = await fetchDistricts(); const s = await fetchDistricts();
districts.value = s;
ongoingFetch.value = null; ongoingFetch.value = null;
return s; return s;
} }
return { return {
error, // Actions
get, byURI,
list,
}; };
}); });

View file

@ -30,18 +30,18 @@ body > .container-fluid {
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { computedAsync } from "@vueuse/core"; 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 Intro from "@/rmo/content/compliance/Intro.vue";
import type { District } from "@/type/api"; import type { District } from "@/type/api";
interface Props { interface Props {
slug: string; slug: string;
} }
const districtStore = useDistrictStore(); const districtStore = useStoreDistrict();
const props = defineProps<Props>(); const props = defineProps<Props>();
const district = computedAsync(async (): Promise<District | undefined> => { 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); return districts.find((district: District) => district.slug == props.slug);
}); });
</script> </script>

View 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>

View file

@ -46,17 +46,17 @@ import { ref } from "vue";
import { computedAsync } from "@vueuse/core"; import { computedAsync } from "@vueuse/core";
import Home from "@/rmo/content/Home.vue"; import Home from "@/rmo/content/Home.vue";
import type { District } from "@/type/api"; import type { District } from "@/type/api";
import { useDistrictStore } from "@/rmo/store/district"; import { useStoreDistrict } from "@/rmo/store/district";
import HeaderDistrict from "@/components/HeaderDistrict.vue"; import HeaderDistrict from "@/components/HeaderDistrict.vue";
interface Props { interface Props {
slug: string; slug: string;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const districtStore = useDistrictStore(); const districtStore = useStoreDistrict();
const district = computedAsync(async (): Promise<District | undefined> => { 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); return districts.find((district: District) => district.slug == props.slug);
}); });
</script> </script>

View file

@ -13,17 +13,17 @@ import { ref } from "vue";
import { computedAsync } from "@vueuse/core"; import { computedAsync } from "@vueuse/core";
import Nuisance from "@/rmo/content/Nuisance.vue"; import Nuisance from "@/rmo/content/Nuisance.vue";
import type { District } from "@/type/api"; import type { District } from "@/type/api";
import { useDistrictStore } from "@/rmo/store/district"; import { useStoreDistrict } from "@/rmo/store/district";
import HeaderDistrict from "@/components/HeaderDistrict.vue"; import HeaderDistrict from "@/components/HeaderDistrict.vue";
interface Props { interface Props {
slug: string; slug: string;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const districtStore = useDistrictStore(); const districtStore = useStoreDistrict();
const district = computedAsync(async (): Promise<District | undefined> => { 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); return districts.find((district: District) => district.slug == props.slug);
}); });
</script> </script>

View file

@ -13,17 +13,17 @@ import { ref } from "vue";
import { computedAsync } from "@vueuse/core"; import { computedAsync } from "@vueuse/core";
import Status from "@/rmo/content/Status.vue"; import Status from "@/rmo/content/Status.vue";
import type { District } from "@/type/api"; import type { District } from "@/type/api";
import { useDistrictStore } from "@/rmo/store/district"; import { useStoreDistrict } from "@/rmo/store/district";
import HeaderDistrict from "@/components/HeaderDistrict.vue"; import HeaderDistrict from "@/components/HeaderDistrict.vue";
interface Props { interface Props {
slug: string; slug: string;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const districtStore = useDistrictStore(); const storeDistrict = useStoreDistrict();
const district = computedAsync(async (): Promise<District | undefined> => { 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); return districts.find((district: District) => district.slug == props.slug);
}); });
</script> </script>

View file

@ -13,17 +13,17 @@ import { ref } from "vue";
import { computedAsync } from "@vueuse/core"; import { computedAsync } from "@vueuse/core";
import Water from "@/rmo/content/Water.vue"; import Water from "@/rmo/content/Water.vue";
import type { District } from "@/type/api"; import type { District } from "@/type/api";
import { useDistrictStore } from "@/rmo/store/district"; import { useStoreDistrict } from "@/rmo/store/district";
import HeaderDistrict from "@/components/HeaderDistrict.vue"; import HeaderDistrict from "@/components/HeaderDistrict.vue";
interface Props { interface Props {
slug: string; slug: string;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const districtStore = useDistrictStore(); const districtStore = useStoreDistrict();
const district = computedAsync(async (): Promise<District | undefined> => { 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); return districts.find((district: District) => district.slug == props.slug);
}); });
</script> </script>

40
ts/store/publicreport.ts Normal file
View 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,
};
});

View file

@ -1,7 +1,7 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref, computed } from "vue"; import { ref } from "vue";
import { Signal } from "../types"; import { Signal } from "@/types";
import { SSEManager, type SSEMessage } from "../SSEManager"; import { SSEManager, type SSEMessage } from "@/SSEManager";
import { useSessionStore } from "@/store/session"; import { useSessionStore } from "@/store/session";
export const useSignalStore = defineStore("signal", () => { export const useSignalStore = defineStore("signal", () => {

View file

@ -1,6 +1,6 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
import { Session, User } from "@/types"; import { User } from "@/types";
import { SSEManager, type SSEMessage } from "@/SSEManager"; import { SSEManager, type SSEMessage } from "@/SSEManager";
import { useSessionStore } from "@/store/session"; import { useSessionStore } from "@/store/session";

View file

@ -13,7 +13,9 @@ export interface District {
name: string; name: string;
phone_office: string; phone_office: string;
slug: string; slug: string;
uri: string;
url_logo: string; url_logo: string;
url_website: string;
} }
export interface Location { export interface Location {
latitude: number; latitude: number;
@ -30,3 +32,8 @@ export interface Geocode {
cell: number; cell: number;
location: Location; location: Location;
} }
export interface Publicreport {
district: string;
id: string;
uri: string;
}