diff --git a/api/routes.go b/api/routes.go index b8d24c5d..a861adec 100644 --- a/api/routes.go +++ b/api/routes.go @@ -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") diff --git a/platform/organization.go b/platform/organization.go index f1128a9b..5ef7bd58 100644 --- a/platform/organization.go +++ b/platform/organization.go @@ -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 { diff --git a/platform/publicreport.go b/platform/publicreport.go index 6d168e28..643e1fbd 100644 --- a/platform/publicreport.go +++ b/platform/publicreport.go @@ -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 { diff --git a/platform/publicreport_notification.go b/platform/publicreport_notification.go new file mode 100644 index 00000000..d983cf82 --- /dev/null +++ b/platform/publicreport_notification.go @@ -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 +} diff --git a/platform/report/error_with_code.go b/platform/report/error_with_code.go deleted file mode 100644 index a1733da0..00000000 --- a/platform/report/error_with_code.go +++ /dev/null @@ -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, - } - } -} diff --git a/platform/report/notification.go b/platform/report/notification.go index a558cd5d..cb1a17d8 100644 --- a/platform/report/notification.go +++ b/platform/report/notification.go @@ -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 } diff --git a/platform/report/some_report.go b/platform/report/some_report.go index 8263dea1..2cbd2d77 100644 --- a/platform/report/some_report.go +++ b/platform/report/some_report.go @@ -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 } diff --git a/resource/communication.go b/resource/communication.go index 40aab0f3..cea4b5c7 100644 --- a/resource/communication.go +++ b/resource/communication.go @@ -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) } diff --git a/resource/district.go b/resource/district.go index 22cedb9c..0d438d23 100644 --- a/resource/district.go +++ b/resource/district.go @@ -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 +} diff --git a/resource/nuisance.go b/resource/nuisance.go index 412b5cba..7166fe1c 100644 --- a/resource/nuisance.go +++ b/resource/nuisance.go @@ -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 } diff --git a/resource/publicreport.go b/resource/publicreport.go new file mode 100644 index 00000000..dc9b4c52 --- /dev/null +++ b/resource/publicreport.go @@ -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 +} diff --git a/resource/publicreport_notification.go b/resource/publicreport_notification.go new file mode 100644 index 00000000..ac41b856 --- /dev/null +++ b/resource/publicreport_notification.go @@ -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 +} diff --git a/resource/router.go b/resource/router.go index cb64293d..180bcd30 100644 --- a/resource/router.go +++ b/resource/router.go @@ -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) } diff --git a/rmo/notification.go b/rmo/notification.go index 1fbb8239..90f9f7ad 100644 --- a/rmo/notification.go +++ b/rmo/notification.go @@ -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) -} diff --git a/rmo/routes.go b/rmo/routes.go index 0c47fc32..d3f758af 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -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") } diff --git a/static/static.go b/static/static.go index 2f7ee2e7..4289daa3 100644 --- a/static/static.go +++ b/static/static.go @@ -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 diff --git a/sync/routes.go b/sync/routes.go index 94100928..0f5f953a 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -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") } diff --git a/ts/rmo/App.vue b/ts/rmo/App.vue index 418bb94f..4890db87 100644 --- a/ts/rmo/App.vue +++ b/ts/rmo/App.vue @@ -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(0); const message = ref("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); }); }); diff --git a/ts/rmo/content/Nuisance.vue b/ts/rmo/content/Nuisance.vue index d53c305a..49496622 100644 --- a/ts/rmo/content/Nuisance.vue +++ b/ts/rmo/content/Nuisance.vue @@ -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(""); @@ -551,6 +558,7 @@ const marker = ref(null); const showMore = ref(false); const selectedSuggestion = ref(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"; diff --git a/ts/rmo/router.ts b/ts/rmo/router.ts index c2279b3f..59896a38 100644 --- a/ts/rmo/router.ts +++ b/ts/rmo/router.ts @@ -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", diff --git a/ts/rmo/store/district.ts b/ts/rmo/store/district.ts index 5f34719c..22427fbc 100644 --- a/ts/rmo/store/district.ts +++ b/ts/rmo/store/district.ts @@ -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(null); +export const useStoreDistrict = defineStore("district", () => { + // State + const _byURI = ref>(new Map()); const error = ref(null); const loading = ref(false); const ongoingFetch = ref | null>(null); + // Actions + async function byURI(uri: string): Promise { + 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 { 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 { - if (districts.value != null) { - return districts.value; + async function list(): Promise { + 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, }; }); diff --git a/ts/rmo/view/Compliance.vue b/ts/rmo/view/Compliance.vue index 93ebdf54..42e6b7e5 100644 --- a/ts/rmo/view/Compliance.vue +++ b/ts/rmo/view/Compliance.vue @@ -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(); const district = computedAsync(async (): Promise => { - const districts = await districtStore.get(); + const districts = await districtStore.list(); return districts.find((district: District) => district.slug == props.slug); }); diff --git a/ts/rmo/view/ReportSubmitted.vue b/ts/rmo/view/ReportSubmitted.vue new file mode 100644 index 00000000..6e016677 --- /dev/null +++ b/ts/rmo/view/ReportSubmitted.vue @@ -0,0 +1,326 @@ + + + + diff --git a/ts/rmo/view/district/Home.vue b/ts/rmo/view/district/Home.vue index 5ee10846..00afe061 100644 --- a/ts/rmo/view/district/Home.vue +++ b/ts/rmo/view/district/Home.vue @@ -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(); -const districtStore = useDistrictStore(); +const districtStore = useStoreDistrict(); const district = computedAsync(async (): Promise => { - const districts = await districtStore.get(); + const districts = await districtStore.list(); return districts.find((district: District) => district.slug == props.slug); }); diff --git a/ts/rmo/view/district/Nuisance.vue b/ts/rmo/view/district/Nuisance.vue index 176d1af7..3dce7348 100644 --- a/ts/rmo/view/district/Nuisance.vue +++ b/ts/rmo/view/district/Nuisance.vue @@ -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(); -const districtStore = useDistrictStore(); +const districtStore = useStoreDistrict(); const district = computedAsync(async (): Promise => { - const districts = await districtStore.get(); + const districts = await districtStore.list(); return districts.find((district: District) => district.slug == props.slug); }); diff --git a/ts/rmo/view/district/Status.vue b/ts/rmo/view/district/Status.vue index f9dc215b..bb1eb0b0 100644 --- a/ts/rmo/view/district/Status.vue +++ b/ts/rmo/view/district/Status.vue @@ -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(); -const districtStore = useDistrictStore(); +const storeDistrict = useStoreDistrict(); const district = computedAsync(async (): Promise => { - const districts = await districtStore.get(); + const districts = await storeDistrict.list(); return districts.find((district: District) => district.slug == props.slug); }); diff --git a/ts/rmo/view/district/Water.vue b/ts/rmo/view/district/Water.vue index 6e368fd3..cfa0fbf1 100644 --- a/ts/rmo/view/district/Water.vue +++ b/ts/rmo/view/district/Water.vue @@ -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(); -const districtStore = useDistrictStore(); +const districtStore = useStoreDistrict(); const district = computedAsync(async (): Promise => { - const districts = await districtStore.get(); + const districts = await districtStore.list(); return districts.find((district: District) => district.slug == props.slug); }); diff --git a/ts/store/publicreport.ts b/ts/store/publicreport.ts new file mode 100644 index 00000000..783f2813 --- /dev/null +++ b/ts/store/publicreport.ts @@ -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>(new Map()); + const error = ref(null); + const loading = ref(false); + //const ongoingFetch = ref | null>(null); + + function add(pr: Publicreport) { + _byID.value.set(pr.id, pr); + } + // Actions + async function byID(id: string): Promise { + 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, + }; +}); diff --git a/ts/store/signal.ts b/ts/store/signal.ts index ff238f7f..5d9d2ed0 100644 --- a/ts/store/signal.ts +++ b/ts/store/signal.ts @@ -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", () => { diff --git a/ts/store/user.ts b/ts/store/user.ts index ac0100ff..300177eb 100644 --- a/ts/store/user.ts +++ b/ts/store/user.ts @@ -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"; diff --git a/ts/type/api.ts b/ts/type/api.ts index 86bfe0bb..bc22d6c5 100644 --- a/ts/type/api.ts +++ b/ts/type/api.ts @@ -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; +}