diff --git a/api/api.go b/api/api.go index 4cbac3a1..d9023ec5 100644 --- a/api/api.go +++ b/api/api.go @@ -10,6 +10,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/gorilla/mux" "github.com/rs/zerolog/log" ) @@ -168,7 +169,7 @@ func apiServiceRequest(w http.ResponseWriter, r *http.Request, u platform.User) data := []Renderable{} for _, sr := range requests { - data = append(data, NewResponseServiceRequest(sr)) + data = append(data, types.ServiceRequestFromModel(sr)) } if err := renderList(w, r, data); err != nil { renderShim(w, r, errRender(err)) diff --git a/api/routes.go b/api/routes.go index c406af72..575ba71c 100644 --- a/api/routes.go +++ b/api/routes.go @@ -47,11 +47,14 @@ func AddRoutes(r *mux.Router) { r.HandleFunc("/rmo/nuisance", handlerFormPost(nuisance.Create)).Methods("POST") water := resource.Water(router) r.HandleFunc("/rmo/water", handlerFormPost(water.Create)).Methods("POST") - r.Handle("/service-request", auth.NewEnsureAuth(apiServiceRequest)).Methods("GET") + service_request := resource.ServiceRequest(router) + r.Handle("/service-request", authenticatedHandlerJSONSlice(service_request.List)).Methods("GET") session := resource.Session(router) r.Handle("/session", authenticatedHandlerJSON(session.Get)).Methods("GET").Name("session.get") signal := resource.Signal(r) r.Handle("/signal", authenticatedHandlerJSON(signal.List)).Methods("GET") + sync := resource.Sync(r) + r.Handle("/sync", authenticatedHandlerJSONSlice(sync.List)).Methods("GET") r.Handle("/sudo/email", authenticatedHandlerJSONPost(postSudoEmail)).Methods("POST") r.Handle("/sudo/sms", authenticatedHandlerJSONPost(postSudoSMS)).Methods("POST") r.Handle("/sudo/sse", authenticatedHandlerJSONPost(postSudoSSE)).Methods("POST") diff --git a/api/types.go b/api/types.go index bd76f664..f72825c7 100644 --- a/api/types.go +++ b/api/types.go @@ -7,6 +7,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/aarondl/opt/null" //"github.com/gorilla/mux" "github.com/rs/zerolog/log" @@ -91,7 +92,7 @@ type NoteAudioBreadcrumbPayload struct { type ResponseFieldseeker struct { MosquitoSources []ResponseMosquitoSource `json:"sources"` - ServiceRequests []ResponseServiceRequest `json:"requests"` + ServiceRequests []types.ServiceRequest `json:"requests"` TrapData []ResponseTrapData `json:"traps"` } @@ -234,48 +235,10 @@ func (rtd ResponseNote) Render(w http.ResponseWriter, r *http.Request) error { return nil } -type ResponseServiceRequest struct { - Address string `json:"address"` - AssignedTechnician string `json:"assigned_technician"` - City string `json:"city"` - Created string `json:"created"` - H3Cell int64 `json:"h3cell"` - HasDog *bool `json:"has_dog"` - HasSpanishSpeaker *bool `json:"has_spanish_speaker"` - ID string `json:"id"` - Priority string `json:"priority"` - RecordedDate string `json:"recorded_date"` - Source string `json:"source"` - Status string `json:"status"` - Target string `json:"target"` - Zip string `json:"zip"` -} - -func (srr ResponseServiceRequest) Render(w http.ResponseWriter, r *http.Request) error { - return nil -} - -func NewResponseServiceRequest(sr *models.FieldseekerServicerequest) ResponseServiceRequest { - return ResponseServiceRequest{ - Address: sr.Reqaddr1.GetOr(""), - AssignedTechnician: sr.Assignedtech.GetOr(""), - City: sr.Reqcity.GetOr(""), - Created: formatTime(sr.Creationdate), - //H3Cell: sr.H3Cell, - HasDog: toBool(sr.Dog), - HasSpanishSpeaker: toBool(sr.Spanish), - ID: sr.Globalid.String(), - Priority: sr.Priority.GetOr(""), - Status: sr.Status.GetOr(""), - Source: sr.Source.GetOr(""), - Target: sr.Reqtarget.GetOr(""), - Zip: sr.Reqzip.GetOr(""), - } -} -func NewResponseServiceRequests(requests models.FieldseekerServicerequestSlice) []ResponseServiceRequest { - results := make([]ResponseServiceRequest, 0) +func NewResponseServiceRequests(requests models.FieldseekerServicerequestSlice) []types.ServiceRequest { + results := make([]types.ServiceRequest, 0) for _, i := range requests { - results = append(results, NewResponseServiceRequest(i)) + results = append(results, types.ServiceRequestFromModel(i)) } return results } diff --git a/platform/ios.go b/platform/ios.go index f29af717..5f2abcd4 100644 --- a/platform/ios.go +++ b/platform/ios.go @@ -10,6 +10,23 @@ import ( "github.com/google/uuid" ) +type ClientSync struct { + Fieldseeker FieldseekerRecordsSync + Since time.Time +} + +type FieldseekerRecordsSync struct { + MosquitoSources []MosquitoSource + ServiceRequests models.FieldseekerServicerequestSlice + TrapData models.FieldseekerTraplocationSlice +} + +type MosquitoSource struct { + PointLocation models.FieldseekerPointlocation + Inspections models.FieldseekerMosquitoinspectionSlice + Treatments models.FieldseekerTreatmentSlice +} + func getFieldseekerRecordsSync(ctx context.Context, u User, since *time.Time) (fsync FieldseekerRecordsSync, err error) { db_connection := db.PGInstance.BobDB pl, err := u.Organization.model.Pointlocations().All(ctx, db_connection) diff --git a/platform/lead.go b/platform/lead.go index 4889129e..12f8755b 100644 --- a/platform/lead.go +++ b/platform/lead.go @@ -11,13 +11,14 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/platform/geom" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" ) // Create a lead from the given signal and site -func LeadCreate(ctx context.Context, user User, signal_id int32, site_id int32, pool_location *Location) (*int32, error) { +func LeadCreate(ctx context.Context, user User, signal_id int32, site_id int32, pool_location *types.Location) (*int32, error) { txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) defer txn.Rollback(ctx) if err != nil { diff --git a/platform/service_request.go b/platform/service_request.go new file mode 100644 index 00000000..0b7d6517 --- /dev/null +++ b/platform/service_request.go @@ -0,0 +1,27 @@ +package platform + +import ( + "context" + "fmt" + + //"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" +) + +func ServiceRequestList(ctx context.Context, user User, limit int) ([]*types.ServiceRequest, error) { + syncs, err := models.FieldseekerServicerequests.Query( + models.SelectWhere.FieldseekerServicerequests.OrganizationID.EQ(user.Organization.ID), + //sm.OrderBy(models.FieldseekerServicerequests.Columns.Created).Desc(), + ).All(ctx, db.PGInstance.BobDB) + if err != nil { + return nil, fmt.Errorf("query sync: %w", err) + } + results := make([]*types.ServiceRequest, len(syncs)) + for i, s := range syncs { + r := types.ServiceRequestFromModel(s) + results[i] = &r + } + return results, nil +} diff --git a/platform/signal.go b/platform/signal.go index e3e1f3c6..8cda221d 100644 --- a/platform/signal.go +++ b/platform/signal.go @@ -76,7 +76,7 @@ func SignalCreateFromPublicreport(ctx context.Context, user User, report_id stri } else if report.LocationLatitude.IsValue() && report.LocationLongitude.IsValue() { lat := report.LocationLatitude.MustGet() lng := report.LocationLongitude.MustGet() - site, err := siteFromLocation(ctx, txn, user, Location{ + site, err := siteFromLocation(ctx, txn, user, types.Location{ Latitude: lat, Longitude: lng, }) diff --git a/platform/site.go b/platform/site.go index 78ec235a..c21e1931 100644 --- a/platform/site.go +++ b/platform/site.go @@ -14,7 +14,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform/geocode" - //"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/omitnull" "github.com/stephenafamo/scan" @@ -89,7 +89,7 @@ func siteFromAddressRaw(ctx context.Context, txn bob.Tx, user User, address stri } return siteFromAddress(ctx, txn, user, a.ID) } -func siteFromLocation(ctx context.Context, txn bob.Tx, user User, location Location) (*models.Site, error) { +func siteFromLocation(ctx context.Context, txn bob.Tx, user User, location types.Location) (*models.Site, error) { // Reverse geocode at the location resp, err := geocode.ReverseGeocode(ctx, location) if err != nil { diff --git a/platform/sync.go b/platform/sync.go new file mode 100644 index 00000000..898902c1 --- /dev/null +++ b/platform/sync.go @@ -0,0 +1,27 @@ +package platform + +import ( + "context" + "fmt" + + "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" +) + +func SyncList(ctx context.Context, user User, limit int) ([]*types.Sync, error) { + syncs, err := models.FieldseekerSyncs.Query( + models.SelectWhere.FieldseekerSyncs.OrganizationID.EQ(user.Organization.ID), + sm.OrderBy(models.FieldseekerSyncs.Columns.Created).Desc(), + ).All(ctx, db.PGInstance.BobDB) + if err != nil { + return nil, fmt.Errorf("query sync: %w", err) + } + results := make([]*types.Sync, len(syncs)) + for i, s := range syncs { + r := types.SyncFromModel(s) + results[i] = &r + } + return results, nil +} diff --git a/platform/types/service_request.go b/platform/types/service_request.go new file mode 100644 index 00000000..0e66b5b8 --- /dev/null +++ b/platform/types/service_request.go @@ -0,0 +1,71 @@ +package types + +import ( + "net/http" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/null" + //"github.com/google/uuid" +) + +type ServiceRequest struct { + Address string `json:"address"` + AssignedTechnician string `json:"assigned_technician"` + City string `json:"city"` + Created string `json:"created"` + H3Cell int64 `json:"h3cell"` + HasDog *bool `json:"has_dog"` + HasSpanishSpeaker *bool `json:"has_spanish_speaker"` + ID string `json:"id"` + Priority string `json:"priority"` + RecordedDate string `json:"recorded_date"` + Source string `json:"source"` + Status string `json:"status"` + Target string `json:"target"` + Zip string `json:"zip"` +} + +func ServiceRequestFromModel(sr *models.FieldseekerServicerequest) ServiceRequest { + //log.Debug().Int32("id", m.ID).Float64("lat", m.LocationLatitude.GetOr(0.0)).Float64("lng", m.LocationLongitude.GetOr(0.0)).Msg("converting address") + return ServiceRequest{ + Address: sr.Reqaddr1.GetOr(""), + AssignedTechnician: sr.Assignedtech.GetOr(""), + City: sr.Reqcity.GetOr(""), + Created: formatTime(sr.Creationdate), + //H3Cell: sr.H3Cell, + HasDog: toBool(sr.Dog), + HasSpanishSpeaker: toBool(sr.Spanish), + ID: sr.Globalid.String(), + Priority: sr.Priority.GetOr(""), + Status: sr.Status.GetOr(""), + Source: sr.Source.GetOr(""), + Target: sr.Reqtarget.GetOr(""), + Zip: sr.Reqzip.GetOr(""), + } +} +func (srr ServiceRequest) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +func formatTime(t null.Val[time.Time]) string { + if t.IsNull() { + return "" + } + v := t.MustGet() + return v.Format("2006-01-02T15:04:05.000Z") +} + +func toBool(t null.Val[int32]) *bool { + if t.IsNull() { + return nil + } + val := t.MustGet() + var b bool + if val == 0 { + b = false + } else { + b = true + } + return &b +} diff --git a/platform/types/sync.go b/platform/types/sync.go new file mode 100644 index 00000000..68aea9c8 --- /dev/null +++ b/platform/types/sync.go @@ -0,0 +1,28 @@ +package types + +import ( + "time" + + "github.com/Gleipnir-Technology/nidus-sync/db/models" +) + +type Sync struct { + Created time.Time `json:"created"` + ID int32 `json:"id"` + OrganizationID int32 `json:"organization_id"` + RecordsCreated int32 `json:"records_created"` + RecordsUnchanged int32 `json:"records_unchanged"` + RecordsUpdated int32 `json:"records_updated"` +} + +func SyncFromModel(m *models.FieldseekerSync) Sync { + //log.Debug().Int32("id", m.ID).Float64("lat", m.LocationLatitude.GetOr(0.0)).Float64("lng", m.LocationLongitude.GetOr(0.0)).Msg("converting address") + return Sync{ + Created: m.Created, + ID: m.ID, + OrganizationID: m.OrganizationID, + RecordsCreated: m.RecordsCreated, + RecordsUnchanged: m.RecordsUnchanged, + RecordsUpdated: m.RecordsUpdated, + } +} diff --git a/resource/lead.go b/resource/lead.go index b5572e52..139c36d0 100644 --- a/resource/lead.go +++ b/resource/lead.go @@ -5,6 +5,7 @@ import ( "fmt" 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/gorilla/mux" "net/http" //"github.com/rs/zerolog/log" @@ -21,8 +22,8 @@ func Lead(r *mux.Router) *leadR { } type createLead struct { - PoolLocations map[int]platform.Location `json:"pool_locations"` - SignalIDs []int `json:"signal_ids"` + PoolLocations map[int]types.Location `json:"pool_locations"` + SignalIDs []int `json:"signal_ids"` } type contentListLead struct { Leads []lead `json:"leads"` @@ -44,7 +45,7 @@ func (res *leadR) Create(ctx context.Context, r *http.Request, user platform.Use return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet") } signal_id := req.SignalIDs[0] - var pool_location *platform.Location + var pool_location *types.Location l, ok := req.PoolLocations[signal_id] if ok { pool_location = &l diff --git a/resource/service_request.go b/resource/service_request.go new file mode 100644 index 00000000..d2f44fe8 --- /dev/null +++ b/resource/service_request.go @@ -0,0 +1,34 @@ +package resource + +import ( + "context" + "net/http" + + 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 serviceRequestR struct { + router *router +} + +func ServiceRequest(r *router) *serviceRequestR { + return &serviceRequestR{ + router: r, + } +} + +func (res *serviceRequestR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]*types.ServiceRequest, *nhttp.ErrorWithStatus) { + limit := 20 + if query.Limit != nil { + limit = *query.Limit + } + serviceRequests, err := platform.ServiceRequestList(ctx, user, limit) + if err != nil { + return nil, nhttp.NewError("list signals: %w", err) + } + return serviceRequests, nil +} diff --git a/resource/session.go b/resource/session.go index 25597fc8..894678e0 100644 --- a/resource/session.go +++ b/resource/session.go @@ -51,6 +51,7 @@ type sessionURLAPI struct { Impersonation string `json:"impersonation"` PublicreportMessage string `json:"publicreport_message"` ReviewTask string `json:"review_task"` + ServiceRequest string `json:"service_request"` Signal string `json:"signal"` Sync string `json:"sync"` Upload string `json:"upload"` @@ -96,6 +97,7 @@ func (res *sessionR) Get(ctx context.Context, r *http.Request, user platform.Use Impersonation: config.MakeURLNidus("/api/impersonation"), PublicreportMessage: urls.API.Publicreport.Message, ReviewTask: config.MakeURLNidus("/api/review-task"), + ServiceRequest: config.MakeURLNidus("/api/service-request"), Signal: config.MakeURLNidus("/api/signal"), Sync: config.MakeURLNidus("/api/sync"), Upload: config.MakeURLNidus("/api/upload"), diff --git a/resource/sync.go b/resource/sync.go new file mode 100644 index 00000000..0b208532 --- /dev/null +++ b/resource/sync.go @@ -0,0 +1,34 @@ +package resource + +import ( + "context" + "net/http" + + 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 syncR struct { + router *mux.Router +} + +func Sync(r *mux.Router) *syncR { + return &syncR{ + router: r, + } +} + +func (res *syncR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]*types.Sync, *nhttp.ErrorWithStatus) { + limit := 20 + if query.Limit != nil { + limit = *query.Limit + } + syncs, err := platform.SyncList(ctx, user, limit) + if err != nil { + return nil, nhttp.NewError("list signals: %w", err) + } + return syncs, nil +} diff --git a/ts/store/service_request.ts b/ts/store/service_request.ts new file mode 100644 index 00000000..1b177f37 --- /dev/null +++ b/ts/store/service_request.ts @@ -0,0 +1,52 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { ServiceRequest } from "@/type/api"; +import { SSEManager, SSEMessage } from "@/SSEManager"; +import { useSessionStore } from "@/store/session"; + +export const useStoreServiceRequest = defineStore("service-request", () => { + // State + const all = ref(null); + const loading = ref(false); + const error = ref(null); + + // Subscription + SSEManager.subscribe((msg: SSEMessage) => { + if (msg.resource.startsWith("sync:service-request")) { + fetchAll(); + } + }); + // Actions + async function fetchAll(): Promise { + const session = useSessionStore(); + if (session.urls == null) { + throw new Error("can't fetch without user URL data"); + } + + loading.value = true; + error.value = null; + try { + const params = new URLSearchParams(); + + const response = await fetch( + `${session.urls.api.service_request}?${params}`, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = (await response.json()) as ServiceRequest[]; + all.value = data; + return data; + } catch (err) { + console.error("Error loading communications:", err); + throw err; + } + } + return { + // State + all, + // Actions + fetchAll, + }; +}); diff --git a/ts/type/api.ts b/ts/type/api.ts index 959a1739..7e76f704 100644 --- a/ts/type/api.ts +++ b/ts/type/api.ts @@ -637,6 +637,77 @@ export interface Session { self: User; urls: URLs; } +export interface ServiceRequestDTO { + address: string; + assigned_technician: string; + city: string; + created: string; + h3cell: number; + has_dog: boolean; + has_spanish_speaker: boolean; + id: string; + priority: string; + recorded_date: string; + source: string; + status: string; + target: string; + zip: string; +} +export interface ServiceRequestOptions { + address: string; + assigned_technician: string; + city: string; + created: Date; + h3cell: number; + has_dog: boolean; + has_spanish_speaker: boolean; + id: string; + priority: string; + recorded_date: Date; + source: string; + status: string; + target: string; + zip: string; +} +export class ServiceRequest { + address: string; + assigned_technician: string; + city: string; + created: Date; + h3cell: number; + has_dog: boolean; + has_spanish_speaker: boolean; + id: string; + priority: string; + recorded_date: Date; + source: string; + status: string; + target: string; + zip: string; + constructor(options: ServiceRequestOptions) { + this.address = options.address; + this.assigned_technician = options.assigned_technician; + this.city = options.city; + this.created = options.created; + this.h3cell = options.h3cell; + this.has_dog = options.has_dog; + this.has_spanish_speaker = options.has_spanish_speaker; + this.id = options.id; + this.priority = options.priority; + this.recorded_date = options.recorded_date; + this.source = options.source; + this.status = options.status; + this.target = options.target; + this.zip = options.zip; + } + static fromJSON(json: ServiceRequestDTO): ServiceRequest { + return new ServiceRequest({ + ...json, + created: new Date(json.created), + recorded_date: new Date(json.recorded_date), + }); + } +} export interface SyncDTO { created: Date; id: string; @@ -677,6 +748,7 @@ interface URLsAPI { impersonation: string; publicreport_message: string; review_task: string; + service_request: string; signal: string; sync: string; upload: string; diff --git a/ts/view/Home.vue b/ts/view/Home.vue index 4126df6a..e348bf58 100644 --- a/ts/view/Home.vue +++ b/ts/view/Home.vue @@ -159,6 +159,7 @@ import { onMounted, reactive } from "vue"; import MapAggregate from "@/components/MapAggregate.vue"; import { formatBigNumber, formatTimeRelative } from "@/format"; import { useSessionStore } from "@/store/session"; +import { useStoreServiceRequest } from "@/store/service_request"; import { useStoreSync } from "@/store/sync"; import type { Bounds } from "@/type/api"; @@ -181,9 +182,11 @@ const dashboard = reactive({ }, recentRequests: [], }); +const storeServiceRequest = useStoreServiceRequest(); const storeSync = useStoreSync(); const session = useSessionStore(); onMounted(async () => { + const service_requests = await storeServiceRequest.fetchAll(); const syncs = await storeSync.fetchAll(); console.log("syncs", syncs); });