diff --git a/endpoint.go b/endpoint.go index 6520ffb3..ef32c882 100644 --- a/endpoint.go +++ b/endpoint.go @@ -233,9 +233,20 @@ func getSignin(w http.ResponseWriter, r *http.Request) { errorCode := r.URL.Query().Get("error") htmlSignin(w, errorCode) } + func getSignup(w http.ResponseWriter, r *http.Request) { htmlSignup(w, r.URL.Path) } + +func getSource(w http.ResponseWriter, r *http.Request, u *models.User) { + globalid := chi.URLParam(r, "globalid") + if globalid == "" { + respondError(w, "No globalid provided", nil, http.StatusBadRequest) + return + } + htmlSource(w, r, u, globalid) +} + func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) { org_id := chi.URLParam(r, "org_id") tileset_id := chi.URLParam(r, "tileset_id") diff --git a/html.go b/html.go index 6667f2c0..f3036821 100644 --- a/html.go +++ b/html.go @@ -32,6 +32,7 @@ var ( dashboard = newBuiltTemplate("dashboard", "authenticated") oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated") settings = newBuiltTemplate("settings", "authenticated") + source = newBuiltTemplate("source", "authenticated") ) // Unauthenticated pages @@ -57,7 +58,7 @@ var ( ) var components = [...]string{"header", "map"} -type BreedingSource struct { +type BreedingSourceSummary struct { ID string Type string LastInspected *time.Time @@ -70,17 +71,21 @@ type BuiltTemplate struct { template *template.Template } +type MapMarker struct { + LatLng LatLng +} type ComponentMap struct { Center LatLng GeoJSON interface{} MapboxToken string + Markers []MapMarker Zoom int } type ContentAuthenticatedPlaceholder struct { User User } type ContentCell struct { - BreedingSources []BreedingSource + BreedingSources []BreedingSourceSummary CellBoundary h3.CellBoundary Inspections []Inspection MapData ComponentMap @@ -114,6 +119,11 @@ type ContentSignin struct { InvalidCredentials bool } type ContentSignup struct{} +type ContentSource struct { + MapData ComponentMap + Source *BreedingSourceDetail + User User +} type LatLng struct { Lat float64 Lng float64 @@ -467,6 +477,47 @@ func htmlSignup(w http.ResponseWriter, path string) { renderOrError(w, signup, data) } +func htmlSource(w http.ResponseWriter, r *http.Request, user *models.User, id string) { + org, err := user.Organization().One(r.Context(), PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to get org", err, http.StatusInternalServerError) + return + } + userContent, err := contentForUser(r.Context(), user) + if err != nil { + respondError(w, "Failed to get user content", err, http.StatusInternalServerError) + return + } + s, err := sourceByGlobalId(r.Context(), org, id) + if err != nil { + respondError(w, "Failed to get source", err, http.StatusInternalServerError) + return + } + data := ContentSource{ + MapData: ComponentMap{ + Center: LatLng{ + Lat: s.GeometryY, + Lng: s.GeometryX, + }, + //GeoJSON: + MapboxToken: MapboxToken, + Markers: []MapMarker{ + MapMarker{ + LatLng: LatLng{ + Lat: s.GeometryY, + Lng: s.GeometryX, + }, + }, + }, + Zoom: 13, + }, + Source: s, + User: userContent, + } + + renderOrError(w, source, data) +} + func gisStatement(cb h3.CellBoundary) string { var content strings.Builder for i, p := range cb { @@ -671,8 +722,8 @@ func inspectionsByCell(ctx context.Context, org *models.Organization, c h3.Cell) } return results, nil } -func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSource, error) { - var results []BreedingSource +func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSourceSummary, error) { + var results []BreedingSourceSummary boundary, err := c.Boundary() if err != nil { @@ -689,7 +740,7 @@ func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.C return results, fmt.Errorf("Failed to query rows: %w", err) } for _, r := range rows { - results = append(results, BreedingSource{ + results = append(results, BreedingSourceSummary{ ID: r.Globalid, LastInspected: fsTimestampToTime(r.Lastinspectdate), LastTreated: fsTimestampToTime(r.Lasttreatdate), @@ -706,3 +757,13 @@ func uuidShort(uuid string) string { return uuid[:3] + "..." + uuid[len(uuid)-4:] } + +func sourceByGlobalId(ctx context.Context, org *models.Organization, id string) (*BreedingSourceDetail, error) { + row, err := org.FSPointlocations( + sm.Where(models.FSPointlocations.Columns.Globalid.EQ(psql.Arg(id))), + ).One(ctx, PGInstance.BobDB) + if err != nil { + return nil, fmt.Errorf("Failed to get point location: %w", err) + } + return ConvertToDisplayModel(row), nil +} diff --git a/main.go b/main.go index f4de23c3..adf3970b 100644 --- a/main.go +++ b/main.go @@ -113,6 +113,7 @@ func main() { // Authenticated endpoints r.Method("GET", "/cell/{cell}", NewEnsureAuth(getCellDetails)) r.Method("GET", "/settings", NewEnsureAuth(getSettings)) + r.Method("GET", "/source/{globalid}", NewEnsureAuth(getSource)) r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles)) localFS := http.Dir("./static") diff --git a/migrations/00015_fs_geometry.sql b/migrations/00015_fs_geometry.sql new file mode 100644 index 00000000..383fb959 --- /dev/null +++ b/migrations/00015_fs_geometry.sql @@ -0,0 +1,14 @@ +-- +goose Up +ALTER TABLE fs_pointlocation ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API +UPDATE fs_pointlocation SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857); + +ALTER TABLE fs_treatment ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API +UPDATE fs_treatment SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857); + +ALTER TABLE fs_mosquitoinspection ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API +UPDATE fs_mosquitoinspection SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857); + +-- +goose Down +ALTER TABLE fs_pointlocation DROP COLUMN geom; +ALTER TABLE fs_treatment DROP COLUMN geom; +ALTER TABLE fs_mosquitoinspection DROP COLUMN geom; diff --git a/model_conversion.go b/model_conversion.go new file mode 100644 index 00000000..62611d01 --- /dev/null +++ b/model_conversion.go @@ -0,0 +1,161 @@ +package main + +import ( + "github.com/Gleipnir-Technology/nidus-sync/models" + "github.com/aarondl/opt/null" + "time" +) + +type BreedingSourceDetail struct { + // Basic Information + OrganizationID int32 `json:"organizationId"` + Name string `json:"name"` + Description string `json:"description"` + LocationNumber int64 `json:"locationNumber"` + ObjectID int32 `json:"objectId"` + GlobalID string `json:"globalId"` + ExternalID string `json:"externalId"` + + // Status Information + Active bool `json:"active"` + DeactivateReason string `json:"deactivateReason"` + SourceStatus string `json:"sourceStatus"` + Priority string `json:"priority"` + ScalarPriority int64 `json:"scalarPriority"` + + // Classification + SourceType string `json:"sourceType"` + Habitat string `json:"habitat"` + UseType string `json:"useType"` + WaterOrigin string `json:"waterOrigin"` + Symbology string `json:"symbology"` + + // Geographical Data + X float64 `json:"x"` + Y float64 `json:"y"` + GeometryX float64 `json:"geometryX"` + GeometryY float64 `json:"geometryY"` + Zone string `json:"zone"` + Zone2 string `json:"zone2"` + Jurisdiction string `json:"jurisdiction"` + AccessDescription string `json:"accessDescription"` + + // Inspection Data + LarvaeInspectInterval int16 `json:"larvaeInspectInterval"` + LastInspectionDate time.Time `json:"lastInspectionDate"` + LastInspectionActivity string `json:"lastInspectionActivity"` + LastInspectionActionTaken string `json:"lastInspectionActionTaken"` + LastInspectionAverageLarvae float64 `json:"lastInspectionAverageLarvae"` + LastInspectionAveragePupae float64 `json:"lastInspectionAveragePupae"` + LastInspectionBreeding string `json:"lastInspectionBreeding"` + LastInspectionConditions string `json:"lastInspectionConditions"` + LastInspectionFieldSpecies string `json:"lastInspectionFieldSpecies"` + LastInspectionLifeStages string `json:"lastInspectionLifeStages"` + + // Treatment Data + LastTreatmentDate time.Time `json:"lastTreatmentDate"` + LastTreatmentActivity string `json:"lastTreatmentActivity"` + LastTreatmentProduct string `json:"lastTreatmentProduct"` + LastTreatmentQuantity float64 `json:"lastTreatmentQuantity"` + LastTreatmentQuantityUnit string `json:"lastTreatmentQuantityUnit"` + + // Assignment & Schedule + AssignedTechnician string `json:"assignedTechnician"` + NextActionScheduledDate time.Time `json:"nextActionScheduledDate"` + + // Metadata + Created time.Time `json:"created"` + Creator string `json:"creator"` + EditedAt time.Time `json:"editedAt"` + Editor string `json:"editor"` + Updated time.Time `json:"updated"` + Comments string `json:"comments"` +} + +// ConvertToDisplayModel transforms the DB model into the display model +func ConvertToDisplayModel(source *models.FSPointlocation) *BreedingSourceDetail { + // Helper function to convert unix timestamp to time.Time + toTime := func(val null.Val[int64]) time.Time { + v, ok := val.Get() + if !ok { + return time.UnixMilli(0) + } + t := time.UnixMilli(v) + return t + } + + // Helper function to convert int16 to bool + toBool := func(val null.Val[int16]) bool { + if !val.IsValue() { + return false + } + b := val.MustGet() != 0 + return b + } + + return &BreedingSourceDetail{ + // Basic Information + OrganizationID: source.OrganizationID, + Name: source.Name.MustGet(), + Description: source.Description.MustGet(), + LocationNumber: source.Locationnumber.GetOr(0), + ObjectID: source.Objectid, + GlobalID: source.Globalid, + ExternalID: source.Externalid.GetOr(""), + + // Status Information + Active: toBool(source.Active), + DeactivateReason: source.DeactivateReason.GetOr(""), + SourceStatus: source.Sourcestatus.GetOr(""), + Priority: source.Priority.GetOr(""), + ScalarPriority: source.Scalarpriority.GetOr(0), + + // Classification + SourceType: source.Stype.GetOr(""), + Habitat: source.Habitat.GetOr(""), + UseType: source.Usetype.GetOr(""), + WaterOrigin: source.Waterorigin.GetOr(""), + Symbology: source.Symbology.GetOr(""), + + // Geographical Data + X: source.X.GetOr(0), + Y: source.Y.GetOr(0), + GeometryX: source.GeometryX, + GeometryY: source.GeometryY, + Zone: source.Zone.GetOr(""), + Zone2: source.Zone2.GetOr(""), + Jurisdiction: source.Jurisdiction.GetOr(""), + AccessDescription: source.Accessdesc.GetOr(""), + + // Inspection Data + LarvaeInspectInterval: source.Larvinspectinterval.GetOr(0), + LastInspectionDate: toTime(source.Lastinspectdate), + LastInspectionActivity: source.Lastinspectactivity.GetOr(""), + LastInspectionActionTaken: source.Lastinspectactiontaken.GetOr(""), + LastInspectionAverageLarvae: source.Lastinspectavglarvae.GetOr(0), + LastInspectionAveragePupae: source.Lastinspectavgpupae.GetOr(0), + LastInspectionBreeding: source.Lastinspectbreeding.GetOr(""), + LastInspectionConditions: source.Lastinspectconditions.GetOr(""), + LastInspectionFieldSpecies: source.Lastinspectfieldspecies.GetOr(""), + LastInspectionLifeStages: source.Lastinspectlstages.GetOr(""), + + // Treatment Data + LastTreatmentDate: toTime(source.Lasttreatdate), + LastTreatmentActivity: source.Lasttreatactivity.GetOr(""), + LastTreatmentProduct: source.Lasttreatproduct.GetOr(""), + LastTreatmentQuantity: source.Lasttreatqty.GetOr(0), + LastTreatmentQuantityUnit: source.Lasttreatqtyunit.GetOr(""), + + // Assignment & Schedule + AssignedTechnician: source.Assignedtech.GetOr(""), + NextActionScheduledDate: toTime(source.Nextactiondatescheduled), + + // Metadata + Created: toTime(source.Creationdate), + Creator: source.Creator.GetOr(""), + EditedAt: toTime(source.Editdate), + Editor: source.Editor.GetOr(""), + Updated: source.Updated, + Comments: source.Comments.GetOr(""), + } +} diff --git a/templates/components/map.html b/templates/components/map.html index 99fbb239..a969cf49 100644 --- a/templates/components/map.html +++ b/templates/components/map.html @@ -3,6 +3,21 @@