diff --git a/api/routes.go b/api/routes.go index 6b202d19..aad9f9b1 100644 --- a/api/routes.go +++ b/api/routes.go @@ -38,6 +38,9 @@ func AddRoutes(r *mux.Router) { lead := resource.Lead(r) r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET") r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST") + mailer := resource.Mailer(router) + r.Handle("/mailer", authenticatedHandlerJSONSlice(mailer.List)).Methods("GET") + r.Handle("/mailer/{id}", authenticatedHandlerJSONPost(mailer.ByIDGet)).Methods("GET").Name("mailer.ByIDGet") r.Handle("/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource)).Methods("GET") r.Handle("/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid)).Methods("POST") diff --git a/platform/mailer.go b/platform/mailer.go new file mode 100644 index 00000000..4f306071 --- /dev/null +++ b/platform/mailer.go @@ -0,0 +1,88 @@ +package platform + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + "github.com/stephenafamo/scan" +) + +func MailerByID(ctx context.Context, user User, id int32) (*types.Mailer, error) { + query := mailerQuery() + query.Apply( + sm.Where(models.ComplianceReportRequests.Columns.ID.EQ(psql.Arg(id))), + sm.Where( + models.Sites.Columns.OrganizationID.EQ(psql.Arg(user.Organization.ID)), + ), + ) + mailers, err := mailerQueryToRows(ctx, query) + if err != nil { + return nil, err + } + return mailers[id], nil +} +func MailerList(ctx context.Context, user User, limit int) ([]*types.Mailer, error) { + query := mailerQuery() + query.Apply( + sm.Where( + models.Sites.Columns.OrganizationID.EQ(psql.Arg(user.Organization.ID)), + ), + sm.OrderBy(models.ComplianceReportRequests.Columns.Created), + sm.Limit(limit), + ) + return mailerQueryToRows(ctx, query) +} +func mailerQuery() bob.BaseQuery[*dialect.SelectQuery] { + return psql.Select( + sm.Columns( + models.Addresses.Columns.Country.As("address.country"), + models.Addresses.Columns.Locality.As("address.locality"), + //sm.From(psql.F("COALESCE", psql.Raw("address.location_latitude"), 0)).As("address.location.latitude"), + //sm.From(psql.F("COALESCE", psql.Raw("address.location_longitude"), 0)).As("address.location.longitude"), + "COALESCE(address.location_latitude, 0) AS \"address.location.latitude\"", + "COALESCE(address.location_longitude, 0) AS \"address.location.longitude\"", + models.Addresses.Columns.Number.As("address.number"), + models.Addresses.Columns.PostalCode.As("address.postal_code"), + models.Addresses.Columns.Region.As("address.region"), + models.Addresses.Columns.Street.As("address.street"), + models.Addresses.Columns.Unit.As("address.unit"), + models.ComplianceReportRequests.Columns.Created.As("created"), + models.ComplianceReportRequests.Columns.PublicID.As("compliance_report_request_id"), + models.Sites.Columns.ID.As("site_id"), + models.Sites.Columns.OwnerName.As("recipient"), + "'created' AS \"status\"", + ), + sm.From(models.ComplianceReportRequestMailers.Name()), + sm.InnerJoin(models.ComplianceReportRequests.Name()).OnEQ( + models.ComplianceReportRequestMailers.Columns.ComplianceReportRequestID, + models.ComplianceReportRequests.Columns.ID, + ), + sm.InnerJoin(models.Leads.Name()).OnEQ( + models.ComplianceReportRequests.Columns.LeadID, + models.Leads.Columns.ID, + ), + sm.InnerJoin(models.Sites.Name()).OnEQ( + models.Leads.Columns.SiteID, + models.Sites.Columns.ID, + ), + sm.InnerJoin(models.Addresses.Name()).OnEQ( + models.Sites.Columns.AddressID, + models.Addresses.Columns.ID, + ), + ) +} +func mailerQueryToRows(ctx context.Context, query bob.BaseQuery[*dialect.SelectQuery]) ([]*types.Mailer, error) { + rows, err := bob.All(ctx, db.PGInstance.BobDB, query, scan.StructMapper[*types.Mailer]()) + if err != nil { + return nil, fmt.Errorf("query mailers: %w", err) + } + + return rows, nil +} diff --git a/platform/types/mailer.go b/platform/types/mailer.go new file mode 100644 index 00000000..980d4a0b --- /dev/null +++ b/platform/types/mailer.go @@ -0,0 +1,16 @@ +package types + +import ( + "time" +) + +type Mailer struct { + Address Address `json:"address"` + ComplianceReportRequestID *string `json:"compliance_report_request_id"` + Created time.Time `json:"created"` + ID int32 `json:"id"` + Recipient string `json:"recipient"` + SiteID int32 `json:"site_id"` + Status string `json:"status"` + URI string `json:"uri"` +} diff --git a/resource/mailer.go b/resource/mailer.go new file mode 100644 index 00000000..7b600339 --- /dev/null +++ b/resource/mailer.go @@ -0,0 +1,55 @@ +package resource + +import ( + "context" + "net/http" + "strconv" + + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + //"github.com/aarondl/opt/null" + "github.com/gorilla/mux" +) + +type mailerR struct { + router *router +} + +func Mailer(r *router) *mailerR { + return &mailerR{ + router: r, + } +} + +func (res *mailerR) ByIDGet(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*types.Mailer, *nhttp.ErrorWithStatus) { + vars := mux.Vars(r) + id_str := vars["id"] + id, err := strconv.Atoi(id_str) + if err != nil { + return nil, nhttp.NewBadRequest("'%s' is not a valid mailer ID: %w", id_str, err) + } + mailer, err := platform.MailerByID(ctx, user, int32(id)) + if err != nil { + return nil, nhttp.NewError("mailer by id: %w", err) + } + return mailer, nil +} +func (res *mailerR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]*types.Mailer, *nhttp.ErrorWithStatus) { + limit := 1000 + if query.Limit != nil { + limit = *query.Limit + } + mailers, err := platform.MailerList(ctx, user, limit) + if err != nil { + return nil, nhttp.NewError("list signals: %w", err) + } + for _, mailer := range mailers { + uri, err := res.router.IDToURI("mailer.ByIDGet", int(mailer.ID)) + if err != nil { + return nil, nhttp.NewError("set uri: %w", err) + } + mailer.URI = uri + } + return mailers, nil +} diff --git a/resource/session.go b/resource/session.go index 3a20c8ec..733c5cc4 100644 --- a/resource/session.go +++ b/resource/session.go @@ -50,6 +50,7 @@ type sessionURLAPI struct { Avatar string `json:"avatar"` Communication string `json:"communication"` Impersonation string `json:"impersonation"` + Mailer string `json:"mailer"` PublicreportMessage string `json:"publicreport_message"` ReviewTask string `json:"review_task"` ServiceRequest string `json:"service_request"` @@ -98,6 +99,7 @@ func (res *sessionR) Get(ctx context.Context, r *http.Request, user platform.Use Avatar: config.MakeURLNidus("/api/avatar"), Communication: urls.API.Communication, Impersonation: config.MakeURLNidus("/api/impersonation"), + Mailer: config.MakeURLNidus("/api/mailer"), PublicreportMessage: urls.API.Publicreport.Message, ReviewTask: config.MakeURLNidus("/api/review-task"), ServiceRequest: config.MakeURLNidus("/api/service-request"), diff --git a/ts/format.ts b/ts/format.ts index e4390907..e183ea2e 100644 --- a/ts/format.ts +++ b/ts/format.ts @@ -24,6 +24,16 @@ export function formatBigNumber(n: number): string { return result; } +export function formatDate(date: Date): string { + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); +} + export function formatDistance(meters: number | undefined) { if (meters === undefined || meters === null) { return "unknown"; diff --git a/ts/router.ts b/ts/router.ts index c83b9440..e32338ef 100644 --- a/ts/router.ts +++ b/ts/router.ts @@ -25,6 +25,7 @@ import NotFound from "@/view/NotFound.vue"; import OAuthRefreshArcgis from "@/view/OAuthRefreshArcgis.vue"; import Operations from "@/view/Operations.vue"; import Planning from "@/view/Planning.vue"; +import ReviewMailer from "@/view/review/Mailer.vue"; import ReviewPool from "@/view/review/Pool.vue"; import ReviewRoot from "@/view/review/Root.vue"; import ReviewSite from "@/view/review/Site.vue"; @@ -153,6 +154,11 @@ const routes: RouteRecordRaw[] = [ name: "Review", component: ReviewRoot, }, + { + path: "/_/review/mailer", + name: "Mailer Review", + component: ReviewMailer, + }, { path: "/_/review/pool", name: "Pool Review", diff --git a/ts/store/mailer.ts b/ts/store/mailer.ts new file mode 100644 index 00000000..09c10c99 --- /dev/null +++ b/ts/store/mailer.ts @@ -0,0 +1,85 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { useSessionStore } from "@/store/session"; + +import { apiClient } from "@/client"; +import { Mailer, type MailerDTO } from "@/type/api"; + +export const useStoreMailer = defineStore("publicreport", () => { + // State + const _all = ref(null); + const _byID = ref>(new Map()); + const loading = ref(false); + const ongoingFetch = ref | null>(null); + + // Actions + async function byID(id: string): Promise { + const r = _byID.value.get(id); + if (r) { + return r; + } + return fetchByID(id); + } + async function byURI(uri: string): Promise { + const id = uri.split("/").pop() || ""; + if (!id) { + throw new Error(`${uri} is not a recognized public report URI`); + } + return byID(id); + } + async function fetchAll(): Promise { + const sessionStore = useSessionStore(); + const session = await sessionStore.get(); + loading.value = true; + const params = new URLSearchParams(); + params.append("sort", "-created"); + const url = `${session.urls.api.mailer}?${params}`; + const mailers = (await apiClient.JSONGet(url)) as Mailer[]; + _all.value = mailers; + for (const m of mailers) { + _byID.value.set(m.id, m); + } + return mailers; + } + async function fetchByID(id: string): Promise { + const uri = `/api/publicreport/${id}`; + return fetchByURI(uri); + } + async function fetchByURI(uri: string): Promise { + loading.value = true; + try { + const response = await fetch(uri); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const body: MailerDTO = await response.json(); + const report = Mailer.fromJSON(body); + _byID.value.set(report.id, report); + return report; + } catch (err) { + console.error("Error loading users:", err); + throw err; + } + } + async function list(): Promise { + if (_all.value) { + return _all.value; + } + if (ongoingFetch.value !== null) { + return ongoingFetch.value; + } + ongoingFetch.value = fetchAll().finally(() => { + ongoingFetch.value = null; + }); + return ongoingFetch.value; + } + return { + // Actions + byID, + byURI, + fetchByID, + fetchByURI, + list, + }; +}); diff --git a/ts/type/api.ts b/ts/type/api.ts index da668750..49edbcbb 100644 --- a/ts/type/api.ts +++ b/ts/type/api.ts @@ -659,6 +659,54 @@ export interface User { uri: string; username: string; } +type MailerStatus = "created" | "printed" | "mailed" | "completed"; +export interface MailerDTO { + address: Address; + compliance_report_request_id?: string; + created: string; + id: string; + recipient: string; + status: MailerStatus; + site_id: string; + uri: string; +} +export interface MailerOptions { + address: Address; + compliance_report_request_id?: string; + created: Date; + id: string; + recipient: string; + site_id: string; + status: MailerStatus; + uri: string; +} +export class Mailer { + address: Address; + compliance_report_request_id?: string; + created: Date; + id: string; + recipient: string; + site_id: string; + status: MailerStatus; + uri: string; + constructor(options: MailerOptions) { + this.address = options.address; + this.compliance_report_request_id = options.compliance_report_request_id; + this.created = options.created; + this.id = options.id; + this.recipient = options.recipient; + this.site_id = options.site_id; + this.status = options.status; + this.uri = options.uri; + } + static fromJSON(json: MailerDTO): Mailer { + return new Mailer({ + ...json, + created: new Date(json.created), + }); + } +} + export interface Organization { id: number; name: string; @@ -791,6 +839,7 @@ interface URLsAPI { avatar: string; communication: string; impersonation: string; + mailer: string; publicreport_message: string; review_task: string; service_request: string; diff --git a/ts/view/review/Mailer.vue b/ts/view/review/Mailer.vue new file mode 100644 index 00000000..c8033535 --- /dev/null +++ b/ts/view/review/Mailer.vue @@ -0,0 +1,165 @@ + + + +