From 21b7b68f503c6991ea5332b509c6edc5703a7999 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 31 Mar 2026 14:52:53 +0000 Subject: [PATCH] Get new frontend to type check clean Epic undertaking. --- api/review_task.go | 3 +- api/user.go | 7 +- db/dbinfo/user_.bob.go | 42 +- db/models/user_.bob.go | 109 ++++- package.json | 3 +- platform/organization.go | 4 +- platform/pool.go | 6 +- platform/site.go | 1 + platform/types/report.go | 2 +- platform/types/site.go | 21 + platform/user.go | 3 +- pnpm-lock.yaml | 96 +++++ ts/App.vue | 10 +- ts/client.ts | 15 +- ts/components/CommunicationColumnAction.vue | 39 +- ts/components/CommunicationColumnDetail.vue | 41 +- ts/components/CommunicationColumnList.vue | 56 ++- ts/components/FlyoverPoolCard.vue | 28 +- ...toViewerModal.vue => ImageViewerModal.vue} | 42 +- ts/components/MapAggregate.vue | 279 ++++++------ ts/components/MapMultipoint.vue | 52 ++- ts/components/MapProxiedArcgisTile.vue | 100 ++--- ts/components/PlanningColumnAction.vue | 2 +- ts/components/PlanningColumnDetail.vue | 87 ++-- ts/components/PlanningColumnDetailEntry.vue | 12 +- ts/components/PlanningColumnList.vue | 31 +- ts/components/PlanningColumnListEntry.vue | 8 +- ts/components/PublicreportCard.vue | 7 +- ts/components/ReviewPoolColumnAction.vue | 9 +- ts/components/ReviewPoolColumnDetail.vue | 92 ++-- ts/components/ReviewPoolColumnList.vue | 9 +- ts/components/layout/Sidebar.vue | 21 +- ts/format.ts | 19 +- ts/store/communication.ts | 11 +- ts/store/review-task.ts | 40 +- ts/store/session.ts | 62 +++ ts/store/signal.ts | 8 +- ts/store/upload.ts | 24 +- ts/store/user.ts | 104 ----- ts/store/users.ts | 20 +- ts/types.ts | 247 ++++++++++- ts/view/Communication.vue | 131 +++--- ts/view/Home.vue | 32 +- ts/view/Planning.vue | 69 +-- ts/view/Sudo.vue | 17 +- ts/view/configuration/Upload.vue | 5 +- ts/view/configuration/UploadDetail.vue | 396 +++++++++--------- ts/view/configuration/UploadPoolFlyover.vue | 6 +- ts/view/configuration/User.vue | 32 +- ts/view/configuration/UserEdit.vue | 100 +++-- ts/view/review/Pool.vue | 174 ++++---- vite.config.ts | 8 +- 52 files changed, 1616 insertions(+), 1126 deletions(-) create mode 100644 platform/types/site.go rename ts/components/{PhotoViewerModal.vue => ImageViewerModal.vue} (69%) create mode 100644 ts/store/session.ts delete mode 100644 ts/store/user.ts diff --git a/api/review_task.go b/api/review_task.go index 3aaf917a..3debaa52 100644 --- a/api/review_task.go +++ b/api/review_task.go @@ -27,7 +27,8 @@ type reviewTask struct { Reviewer *platform.User `json:"addressor"` } type reviewTaskPool struct { - Condition string `json:"condition"` + Condition string `json:"condition"` + Site types.Site `json:"site"` } type contentListReviewTask struct { Tasks []reviewTask `json:"tasks"` diff --git a/api/user.go b/api/user.go index 4b5f0cb7..0fc62f20 100644 --- a/api/user.go +++ b/api/user.go @@ -17,7 +17,7 @@ type contentURLAPI struct { ReviewTask string `json:"review_task"` Signal string `json:"signal"` Upload string `json:"upload"` - Users string `json:"users"` + User string `json:"users"` } type contentURLs struct { API contentURLAPI `json:"api"` @@ -51,7 +51,7 @@ func getUserSelf(ctx context.Context, r *http.Request, user platform.User, query ReviewTask: config.MakeURLNidus("/api/review-task"), Signal: config.MakeURLNidus("/api/signal"), Upload: config.MakeURLNidus("/api/upload"), - Users: config.MakeURLNidus("/api/user"), + User: config.MakeURLNidus("/api/user"), }, Tegola: urls.Tegola, Tile: config.MakeURLNidus("/api/tile/{z}/{y}/{x}"), @@ -96,6 +96,7 @@ func listUserSuggestion(ctx context.Context, r *http.Request, user platform.User }, nil } -func userPut(ctx context.Context, r *http.Request, user platform.User, updates platform.User) { +func userPut(ctx context.Context, r *http.Request, user platform.User, updates platform.User) (string, *nhttp.ErrorWithStatus) { //if updates.Avatar + return "", nil } diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index fcee24c3..9bbd0e95 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -132,6 +132,42 @@ var Users = Table[ Generated: false, AutoIncr: false, }, + IsActive: column{ + Name: "is_active", + DBType: "boolean", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + IsDronePilot: column{ + Name: "is_drone_pilot", + DBType: "boolean", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, + IsWarrant: column{ + Name: "is_warrant", + DBType: "boolean", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, + Avatar: column{ + Name: "avatar", + DBType: "uuid", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, }, Indexes: userIndexes{ UserPkey: index{ @@ -210,11 +246,15 @@ type userColumns struct { PasswordHashType column PasswordHash column Role column + IsActive column + IsDronePilot column + IsWarrant column + Avatar column } func (c userColumns) AsSlice() []column { return []column{ - c.ID, c.ArcgisAccessToken, c.ArcgisLicense, c.ArcgisRefreshToken, c.ArcgisRefreshTokenExpires, c.ArcgisRole, c.DisplayName, c.Email, c.OrganizationID, c.Username, c.PasswordHashType, c.PasswordHash, c.Role, + c.ID, c.ArcgisAccessToken, c.ArcgisLicense, c.ArcgisRefreshToken, c.ArcgisRefreshTokenExpires, c.ArcgisRole, c.DisplayName, c.Email, c.OrganizationID, c.Username, c.PasswordHashType, c.PasswordHash, c.Role, c.IsActive, c.IsDronePilot, c.IsWarrant, c.Avatar, } } diff --git a/db/models/user_.bob.go b/db/models/user_.bob.go index de084adf..a7fbf9ac 100644 --- a/db/models/user_.bob.go +++ b/db/models/user_.bob.go @@ -22,6 +22,7 @@ import ( "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" + "github.com/google/uuid" ) // User is an object representing the database table. @@ -39,6 +40,10 @@ type User struct { PasswordHashType enums.Hashtype `db:"password_hash_type" ` PasswordHash string `db:"password_hash" ` Role enums.Userrole `db:"role" ` + IsActive bool `db:"is_active" ` + IsDronePilot null.Val[bool] `db:"is_drone_pilot" ` + IsWarrant null.Val[bool] `db:"is_warrant" ` + Avatar null.Val[uuid.UUID] `db:"avatar" ` R userR `db:"-" ` } @@ -88,7 +93,7 @@ type userR struct { func buildUserColumns(alias string) userColumns { return userColumns{ ColumnsExpr: expr.NewColumnsExpr( - "id", "arcgis_access_token", "arcgis_license", "arcgis_refresh_token", "arcgis_refresh_token_expires", "arcgis_role", "display_name", "email", "organization_id", "username", "password_hash_type", "password_hash", "role", + "id", "arcgis_access_token", "arcgis_license", "arcgis_refresh_token", "arcgis_refresh_token_expires", "arcgis_role", "display_name", "email", "organization_id", "username", "password_hash_type", "password_hash", "role", "is_active", "is_drone_pilot", "is_warrant", "avatar", ).WithParent("user_"), tableAlias: alias, ID: psql.Quote(alias, "id"), @@ -104,6 +109,10 @@ func buildUserColumns(alias string) userColumns { PasswordHashType: psql.Quote(alias, "password_hash_type"), PasswordHash: psql.Quote(alias, "password_hash"), Role: psql.Quote(alias, "role"), + IsActive: psql.Quote(alias, "is_active"), + IsDronePilot: psql.Quote(alias, "is_drone_pilot"), + IsWarrant: psql.Quote(alias, "is_warrant"), + Avatar: psql.Quote(alias, "avatar"), } } @@ -123,6 +132,10 @@ type userColumns struct { PasswordHashType psql.Expression PasswordHash psql.Expression Role psql.Expression + IsActive psql.Expression + IsDronePilot psql.Expression + IsWarrant psql.Expression + Avatar psql.Expression } func (c userColumns) Alias() string { @@ -150,10 +163,14 @@ type UserSetter struct { PasswordHashType omit.Val[enums.Hashtype] `db:"password_hash_type" ` PasswordHash omit.Val[string] `db:"password_hash" ` Role omit.Val[enums.Userrole] `db:"role" ` + IsActive omit.Val[bool] `db:"is_active" ` + IsDronePilot omitnull.Val[bool] `db:"is_drone_pilot" ` + IsWarrant omitnull.Val[bool] `db:"is_warrant" ` + Avatar omitnull.Val[uuid.UUID] `db:"avatar" ` } func (s UserSetter) SetColumns() []string { - vals := make([]string, 0, 13) + vals := make([]string, 0, 17) if s.ID.IsValue() { vals = append(vals, "id") } @@ -193,6 +210,18 @@ func (s UserSetter) SetColumns() []string { if s.Role.IsValue() { vals = append(vals, "role") } + if s.IsActive.IsValue() { + vals = append(vals, "is_active") + } + if !s.IsDronePilot.IsUnset() { + vals = append(vals, "is_drone_pilot") + } + if !s.IsWarrant.IsUnset() { + vals = append(vals, "is_warrant") + } + if !s.Avatar.IsUnset() { + vals = append(vals, "avatar") + } return vals } @@ -236,6 +265,18 @@ func (s UserSetter) Overwrite(t *User) { if s.Role.IsValue() { t.Role = s.Role.MustGet() } + if s.IsActive.IsValue() { + t.IsActive = s.IsActive.MustGet() + } + if !s.IsDronePilot.IsUnset() { + t.IsDronePilot = s.IsDronePilot.MustGetNull() + } + if !s.IsWarrant.IsUnset() { + t.IsWarrant = s.IsWarrant.MustGetNull() + } + if !s.Avatar.IsUnset() { + t.Avatar = s.Avatar.MustGetNull() + } } func (s *UserSetter) Apply(q *dialect.InsertQuery) { @@ -244,7 +285,7 @@ func (s *UserSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 13) + vals := make([]bob.Expression, 17) if s.ID.IsValue() { vals[0] = psql.Arg(s.ID.MustGet()) } else { @@ -323,6 +364,30 @@ func (s *UserSetter) Apply(q *dialect.InsertQuery) { vals[12] = psql.Raw("DEFAULT") } + if s.IsActive.IsValue() { + vals[13] = psql.Arg(s.IsActive.MustGet()) + } else { + vals[13] = psql.Raw("DEFAULT") + } + + if !s.IsDronePilot.IsUnset() { + vals[14] = psql.Arg(s.IsDronePilot.MustGetNull()) + } else { + vals[14] = psql.Raw("DEFAULT") + } + + if !s.IsWarrant.IsUnset() { + vals[15] = psql.Arg(s.IsWarrant.MustGetNull()) + } else { + vals[15] = psql.Raw("DEFAULT") + } + + if !s.Avatar.IsUnset() { + vals[16] = psql.Arg(s.Avatar.MustGetNull()) + } else { + vals[16] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -332,7 +397,7 @@ func (s UserSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s UserSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 13) + exprs := make([]bob.Expression, 0, 17) if s.ID.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -425,6 +490,34 @@ func (s UserSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if s.IsActive.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "is_active")...), + psql.Arg(s.IsActive), + }}) + } + + if !s.IsDronePilot.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "is_drone_pilot")...), + psql.Arg(s.IsDronePilot), + }}) + } + + if !s.IsWarrant.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "is_warrant")...), + psql.Arg(s.IsWarrant), + }}) + } + + if !s.Avatar.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "avatar")...), + psql.Arg(s.Avatar), + }}) + } + return exprs } @@ -3221,6 +3314,10 @@ type userWhere[Q psql.Filterable] struct { PasswordHashType psql.WhereMod[Q, enums.Hashtype] PasswordHash psql.WhereMod[Q, string] Role psql.WhereMod[Q, enums.Userrole] + IsActive psql.WhereMod[Q, bool] + IsDronePilot psql.WhereNullMod[Q, bool] + IsWarrant psql.WhereNullMod[Q, bool] + Avatar psql.WhereNullMod[Q, uuid.UUID] } func (userWhere[Q]) AliasedAs(alias string) userWhere[Q] { @@ -3242,6 +3339,10 @@ func buildUserWhere[Q psql.Filterable](cols userColumns) userWhere[Q] { PasswordHashType: psql.Where[Q, enums.Hashtype](cols.PasswordHashType), PasswordHash: psql.Where[Q, string](cols.PasswordHash), Role: psql.Where[Q, enums.Userrole](cols.Role), + IsActive: psql.Where[Q, bool](cols.IsActive), + IsDronePilot: psql.WhereNull[Q, bool](cols.IsDronePilot), + IsWarrant: psql.WhereNull[Q, bool](cols.IsWarrant), + Avatar: psql.WhereNull[Q, uuid.UUID](cols.Avatar), } } diff --git a/package.json b/package.json index 070e2b4d..bfcee181 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,11 @@ "sass": "^1.98.0", "typescript": "^5.9.3", "vite": "^8.0.1", + "vite-plugin-checker": "^0.12.0", "vue-tsc": "^3.2.6" }, "scripts": { - "build": "vite build", + "build": "vue-tsc && vite build", "dev": "vite", "generate-icons": "node generate-icons.js", "typecheck": "vue-tsc --noEmit", diff --git a/platform/organization.go b/platform/organization.go index 996d7875..bac16295 100644 --- a/platform/organization.go +++ b/platform/organization.go @@ -76,8 +76,8 @@ func (o Organization) FieldseekerSyncLatest(ctx context.Context) (*models.Fields } type ServiceArea struct { - Min Point - Max Point + Min Point `json:"min"` + Max Point `json:"max"` } func (o Organization) ServiceRequestRecent(ctx context.Context) ([]*models.FieldseekerServicerequest, error) { diff --git a/platform/pool.go b/platform/pool.go index 7b022b6c..a2d02dff 100644 --- a/platform/pool.go +++ b/platform/pool.go @@ -18,9 +18,9 @@ type Pool struct { ID int32 `db:"id" json:"-"` } type UploadPoolError struct { - Column uint - Line uint - Message string + Column uint `json:"column"` + Line uint `json:"line"` + Message string `json:"message"` } func errorsByLine(ctx context.Context, file *models.FileuploadFile) ([]UploadPoolError, map[int32][]UploadPoolError, error) { diff --git a/platform/site.go b/platform/site.go index e4bdf7ed..e54c2338 100644 --- a/platform/site.go +++ b/platform/site.go @@ -14,6 +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/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/stephenafamo/scan" diff --git a/platform/types/report.go b/platform/types/report.go index 1a56b5c6..968660e9 100644 --- a/platform/types/report.go +++ b/platform/types/report.go @@ -5,13 +5,13 @@ import ( ) type Report struct { - Log []LogEntry `db:"-" json:"log"` Address Address `db:"address" json:"address"` AddressRaw string `db:"address_raw" json:"address_raw"` Created time.Time `db:"created" json:"created"` ID int32 `db:"id" json:"-"` Images []Image `db:"images" json:"images"` Location *Location `db:"location" json:"location"` + Log []LogEntry `db:"-" json:"log"` Nuisance *Nuisance `db:"nuisance" json:"nuisance"` PublicID string `db:"public_id" json:"public_id"` Reporter Contact `db:"reporter" json:"reporter"` diff --git a/platform/types/site.go b/platform/types/site.go new file mode 100644 index 00000000..8cbd7f0e --- /dev/null +++ b/platform/types/site.go @@ -0,0 +1,21 @@ +package types + +import ( + "time" +) + +type Site struct { + Address Address `json:"address"` + Created time.Time `json:"created"` + CreatorID int32 `json:"creator_id"` + FileID int32 `json:"file_id"` + ID int32 `json:"id"` + Notes string `json:"notes"` + OrganizationID int32 `json:"organization_id"` + Owner *Contact `json:"owner"` + ParcelID *int32 `json:"parcel_id"` + Resident *Contact `json:"resident"` + ResidentOwned bool `json:"resident_owned"` + Tags map[string]string `json:"tags"` + Version int32 `json:"version"` +} diff --git a/platform/user.go b/platform/user.go index 20c2bd3a..68807e61 100644 --- a/platform/user.go +++ b/platform/user.go @@ -15,6 +15,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/debug" "github.com/aarondl/opt/omit" + "github.com/google/uuid" "github.com/rs/zerolog/log" ) @@ -54,7 +55,7 @@ func (u User) HasRoot() bool { func newUser(ctx context.Context, org Organization, user *models.User) User { u := User{ Active: true, - Avatar: user.Avatar, + Avatar: user.Avatar.GetOr(uuid.UUID{}).String(), DisplayName: user.DisplayName, ID: int(user.ID), Initials: extractInitials(user.DisplayName), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f2ac48a..41eafac7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,12 +48,19 @@ importers: vite: specifier: ^8.0.1 version: 8.0.1(sass@1.98.0)(yaml@2.8.3) + vite-plugin-checker: + specifier: ^0.12.0 + version: 0.12.0(typescript@5.9.3)(vite@8.0.1(sass@1.98.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3)) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) packages: + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} @@ -587,6 +594,9 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -723,9 +733,17 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -825,6 +843,9 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -843,6 +864,10 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unplugin-utils@0.3.1: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} @@ -851,6 +876,43 @@ packages: resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} engines: {node: ^20.19.0 || >=22.12.0} + vite-plugin-checker@0.12.0: + resolution: {integrity: sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==} + engines: {node: '>=16.11'} + peerDependencies: + '@biomejs/biome': '>=1.7' + eslint: '>=9.39.1' + meow: ^13.2.0 + optionator: ^0.9.4 + oxlint: '>=1' + stylelint: '>=16' + typescript: '*' + vite: '>=5.4.21' + vls: '*' + vti: '*' + vue-tsc: ~2.2.10 || ^3.0.0 + peerDependenciesMeta: + '@biomejs/biome': + optional: true + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + oxlint: + optional: true + stylelint: + optional: true + typescript: + optional: true + vls: + optional: true + vti: + optional: true + vue-tsc: + optional: true + vite@8.0.1: resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -936,6 +998,12 @@ packages: snapshots: + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.2 @@ -1456,6 +1524,8 @@ snapshots: is-what@5.5.0: {} + js-tokens@4.0.0: {} + jsesc@3.1.0: {} json-stringify-pretty-compact@4.0.0: {} @@ -1577,8 +1647,15 @@ snapshots: node-addon-api@7.1.1: optional: true + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + path-browserify@1.0.1: {} + path-key@4.0.0: {} + pathe@2.0.3: {} pbf@4.0.1: @@ -1683,6 +1760,8 @@ snapshots: dependencies: copy-anything: 4.0.5 + tiny-invariant@1.3.3: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -1697,6 +1776,8 @@ snapshots: ufo@1.6.3: {} + unicorn-magic@0.3.0: {} + unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 @@ -1708,6 +1789,21 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + vite-plugin-checker@0.12.0(typescript@5.9.3)(vite@8.0.1(sass@1.98.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3)): + dependencies: + '@babel/code-frame': 7.29.0 + chokidar: 4.0.3 + npm-run-path: 6.0.0 + picocolors: 1.1.1 + picomatch: 4.0.3 + tiny-invariant: 1.3.3 + tinyglobby: 0.2.15 + vite: 8.0.1(sass@1.98.0)(yaml@2.8.3) + vscode-uri: 3.1.0 + optionalDependencies: + typescript: 5.9.3 + vue-tsc: 3.2.6(typescript@5.9.3) + vite@8.0.1(sass@1.98.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 diff --git a/ts/App.vue b/ts/App.vue index a6e01d3a..cdfb8186 100644 --- a/ts/App.vue +++ b/ts/App.vue @@ -2,8 +2,8 @@
-
Loading...
-
Error: {{ userStore.error }}
+
Loading...
+
Error: {{ session.error }}
@@ -11,15 +11,15 @@ diff --git a/ts/client.ts b/ts/client.ts index da356f68..ed0e61d8 100644 --- a/ts/client.ts +++ b/ts/client.ts @@ -1,24 +1,31 @@ -// src/api/axios.js or similar -import axios from "axios"; +// src/api/axios.ts +import axios, { AxiosInstance } from "axios"; import router from "@/router"; +// Extend the AxiosInstance interface +declare module "axios" { + interface AxiosInstance { + isAuthenticated(): boolean; + } +} + const apiClient = axios.create({ baseURL: "/api", withCredentials: true, }); -// Response interceptor to catch auth failures apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response && error.response.status === 401) { - // Session expired or not authenticated router.push("/login"); } return Promise.reject(error); }, ); + apiClient.isAuthenticated = () => { return true; }; + export default apiClient; diff --git a/ts/components/CommunicationColumnAction.vue b/ts/components/CommunicationColumnAction.vue index 9de1bce4..b0d1cb7d 100644 --- a/ts/components/CommunicationColumnAction.vue +++ b/ts/components/CommunicationColumnAction.vue @@ -49,8 +49,8 @@
@@ -74,7 +74,7 @@ >
@@ -50,11 +49,10 @@
@@ -65,11 +63,10 @@
-
+
@@ -107,11 +103,10 @@
@@ -120,50 +115,75 @@
-
+
+
+

loading...

+
-
+
diff --git a/ts/components/ReviewPoolColumnList.vue b/ts/components/ReviewPoolColumnList.vue index badacd68..95a25c16 100644 --- a/ts/components/ReviewPoolColumnList.vue +++ b/ts/components/ReviewPoolColumnList.vue @@ -52,7 +52,7 @@ Pool {{ task.id }}
- {{ task.condition }} + {{ task.pool?.condition }} {{ formatAddress(task.address) }} @@ -61,15 +61,16 @@ diff --git a/ts/format.ts b/ts/format.ts index 12c71362..108763c6 100644 --- a/ts/format.ts +++ b/ts/format.ts @@ -9,6 +9,21 @@ export function formatAddress(address?: Address): string { } return `${address.number} ${address.street}, ${address.locality}`; } +export function formatDistance(meters: number | undefined) { + if (meters === undefined || meters === null) { + return "unknown"; + } + if (meters < 1) { + const mm = Math.round(meters * 1000); + return `${mm} mm`; + } else if (meters >= 1000) { + const km = Math.round(meters / 1000); + return `${km} km`; + } else { + const m = Math.round(meters); + return `${m} m`; + } +} export function formatRelativeTime(dateString: string): string { if (!dateString) return ""; @@ -25,7 +40,7 @@ export function formatRelativeTime(dateString: string): string { return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; } -export function shortAddress(a: Address): string { - if (!a) return ""; +export function shortAddress(a: Address | undefined): string { + if (!a) return "unknown"; return `${a.number} ${a.street}, ${a.locality}`; } diff --git a/ts/store/communication.ts b/ts/store/communication.ts index 89317141..61a05dbb 100644 --- a/ts/store/communication.ts +++ b/ts/store/communication.ts @@ -2,7 +2,7 @@ import { defineStore } from "pinia"; import { ref, computed } from "vue"; import { Communication } from "../types"; import { SSEManager } from "../SSEManager"; -import { useUserStore } from "./user"; +import { useSessionStore } from "./session"; export const useCommunicationStore = defineStore("communication", () => { // State @@ -17,9 +17,9 @@ export const useCommunicationStore = defineStore("communication", () => { } }); // Actions - async function fetchAll() { - const userStore = useUserStore(); - if (userStore.urls == null) { + async function fetchAll(): Promise { + const session = useSessionStore(); + if (session.urls == null) { throw new Error("can't fetch without user URL data"); } @@ -31,7 +31,7 @@ export const useCommunicationStore = defineStore("communication", () => { //if (typeFilter.value) params.append("type", typeFilter.value); const response = await fetch( - `${userStore.urls.api.communication}?${params}`, + `${session.urls.api.communication}?${params}`, ); if (!response.ok) { @@ -39,6 +39,7 @@ export const useCommunicationStore = defineStore("communication", () => { } const data = await response.json(); all.value = data.communications; + return data.communications; } catch (err) { console.error("Error loading communications:", err); throw err; diff --git a/ts/store/review-task.ts b/ts/store/review-task.ts index 30925556..89cde256 100644 --- a/ts/store/review-task.ts +++ b/ts/store/review-task.ts @@ -1,15 +1,14 @@ import { defineStore } from "pinia"; import { ref } from "vue"; -import { ReviewTask } from "../types"; -import { SSEManager } from "../SSEManager"; -import { useUserStore } from "./user"; +import { SSEManager } from "@/SSEManager"; +import { ReviewTask } from "@/types"; +import { useSessionStore } from "@/store/session"; export const useReviewTaskStore = defineStore("review-task", () => { // State - const _byID = ref>(new Map()); - const all = ref(null); - const loading = ref(false); - const error = ref(null); + const _byID = ref>(new Map()); + const loading = ref(false); + const error = ref(null); // Subscription SSEManager.subscribe("*", (e) => { @@ -18,12 +17,15 @@ export const useReviewTaskStore = defineStore("review-task", () => { } }); // Actions - function byID(id: int) { + function all(): ReviewTask[] { + return Array.from(_byID.value.values()); + } + function byID(id: number) { return _byID.value.get(id); } async function fetchAll(): Promise { - const userStore = useUserStore(); - if (userStore.urls == null) { + const session = useSessionStore(); + if (session.urls == null) { throw new Error("can't fetch without user URL data"); } @@ -34,15 +36,13 @@ export const useReviewTaskStore = defineStore("review-task", () => { params.append("sort", "-created"); //if (typeFilter.value) params.append("type", typeFilter.value); - const response = await fetch( - `${userStore.urls.api.review_task}?${params}`, - ); + const response = await fetch(`${session.urls.api.review_task}?${params}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); - all.value = data.tasks; + _byID.value = new Map(); for (const t of data.tasks) { _byID.value.set(t.id, t); } @@ -54,16 +54,16 @@ export const useReviewTaskStore = defineStore("review-task", () => { loading.value = false; } } - async function fetchOne(id: int) { - const userStore = useUserStore(); - if (userStore.urls == null) { + async function fetchOne(id: number) { + 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 response = await fetch(`${userStore.urls.api.review_task}/${id}`); + const response = await fetch(`${session.urls.api.review_task}/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -76,6 +76,9 @@ export const useReviewTaskStore = defineStore("review-task", () => { throw err; } } + function remove(id: number) { + _byID.value.delete(id); + } return { // State @@ -84,5 +87,6 @@ export const useReviewTaskStore = defineStore("review-task", () => { byID, fetchAll, fetchOne, + remove, }; }); diff --git a/ts/store/session.ts b/ts/store/session.ts new file mode 100644 index 00000000..273a059a --- /dev/null +++ b/ts/store/session.ts @@ -0,0 +1,62 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { SSEManager } from "@/SSEManager"; +import { + Organization, + URLs, + User, + UserNotificationCounts, + UserResponse, +} from "@/types"; + +export const useSessionStore = defineStore("session", () => { + // State + const error = ref(null); + const loading = ref(false); + const user = ref(null); + const urls = ref(null); + + // Subscription + SSEManager.subscribe("*", (e) => { + if (e.type !== "heartbeat") { + fetchSession(); + } + }); + + // Actions + async function fetchSession(): Promise { + loading.value = true; + error.value = null; + + try { + const response = await fetch("/api/user/self"); + if (!response.ok) throw new Error("Failed to fetch user"); + + const data: UserResponse = await response.json(); + user.value = data.self; + urls.value = data.urls; + return data; + } catch (e) { + error.value = e instanceof Error ? e.message : "an error ocurred"; + console.error("Error fetching user:", e); + throw new Error(error.value); + } finally { + loading.value = false; + } + } + + async function isAuthenticated(): Promise { + console.log("pretend check user auth"); + return true; + } + return { + // State + error, + loading, + user, + urls, + // Actions + fetchSession, + isAuthenticated, + }; +}); diff --git a/ts/store/signal.ts b/ts/store/signal.ts index 638b6761..8da15c99 100644 --- a/ts/store/signal.ts +++ b/ts/store/signal.ts @@ -2,7 +2,7 @@ import { defineStore } from "pinia"; import { ref, computed } from "vue"; import { Signal } from "../types"; import { SSEManager } from "../SSEManager"; -import { useUserStore } from "./user"; +import { useSessionStore } from "@/store/session"; export const useSignalStore = defineStore("signal", () => { // State @@ -18,8 +18,8 @@ export const useSignalStore = defineStore("signal", () => { }); // Actions async function fetchAll() { - const userStore = useUserStore(); - if (userStore.urls == null) { + const session = useSessionStore(); + if (session.urls == null) { throw new Error("can't fetch without user URL data"); } @@ -30,7 +30,7 @@ export const useSignalStore = defineStore("signal", () => { params.append("sort", "-created"); //if (typeFilter.value) params.append("type", typeFilter.value); - const response = await fetch(`${userStore.urls.api.signal}?${params}`); + const response = await fetch(`${session.urls.api.signal}?${params}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); diff --git a/ts/store/upload.ts b/ts/store/upload.ts index f132577d..fcf91947 100644 --- a/ts/store/upload.ts +++ b/ts/store/upload.ts @@ -1,12 +1,12 @@ import { defineStore } from "pinia"; import { ref } from "vue"; -import { Upload } from "../types"; -import { SSEManager } from "../SSEManager"; -import { useUserStore } from "./user"; +import { Upload } from "@/types"; +import { SSEManager } from "@/SSEManager"; +import { useSessionStore } from "@/store/session"; export const useUploadStore = defineStore("upload", () => { // State - const _byID = ref>(new Map()); + const _byID = ref>(new Map()); const all = ref(null); const loading = ref(false); const error = ref(null); @@ -18,12 +18,12 @@ export const useUploadStore = defineStore("upload", () => { } }); // Actions - function byID(id: int) { + function byID(id: number) { return _byID.value.get(id); } async function fetchAll() { - const userStore = useUserStore(); - if (userStore.urls == null) { + const session = useSessionStore(); + if (session.urls == null) { throw new Error("can't fetch without user URL data"); } @@ -34,7 +34,7 @@ export const useUploadStore = defineStore("upload", () => { params.append("sort", "-created"); //if (typeFilter.value) params.append("type", typeFilter.value); - const response = await fetch(`${userStore.urls.api.upload}?${params}`); + const response = await fetch(`${session.urls.api.upload}?${params}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -49,16 +49,16 @@ export const useUploadStore = defineStore("upload", () => { throw err; } } - async function fetchOne(id: int) { - const userStore = useUserStore(); - if (userStore.urls == null) { + async function fetchOne(id: number) { + 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 response = await fetch(`${userStore.urls.api.upload}/${id}`); + const response = await fetch(`${session.urls.api.upload}/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); diff --git a/ts/store/user.ts b/ts/store/user.ts deleted file mode 100644 index 75c14b1f..00000000 --- a/ts/store/user.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { defineStore } from "pinia"; -import { ref, computed } from "vue"; -import { SSEManager } from "../SSEManager"; - -// Define interfaces matching your Go structs -interface URLsAPI { - communication: string; - signal: string; -} - -interface URLs { - api: URLsAPI; - tegola: string; - tile: string; -} - -interface User { - display_name: string; - initials: string; - notification_counts: NotificationCounts; - notifications: any[]; // Replace with proper type - organization: string; // Replace with proper type - role: string; - username: string; -} - -interface UserResponse { - self: User; - urls: URLs; -} - -interface NotificationCounts { - // Add the actual structure based on your API - [key: string]: number; -} - -export const useUserStore = defineStore("user", () => { - // State - const display_name = ref(null); - const error = ref(null); - const initials = ref(null); - const loading = ref(false); - const notification_counts = ref(null); - const notifications = ref(null); - const organization = ref(null); - const role = ref(null); - const urls = ref(null); - const username = ref(null); - - // Subscription - SSEManager.subscribe("*", (e) => { - if (e.type !== "heartbeat") { - fetchUser(); - } - }); - - // Actions - async function fetchUser() { - loading.value = true; - error.value = null; - - try { - const response = await fetch("/api/user/self"); - if (!response.ok) throw new Error("Failed to fetch user"); - - const data: UserResponse = await response.json(); - display_name.value = data.self.display_name; - initials.value = data.self.initials; - notification_counts.value = data.self.notification_counts; - notifications.value = data.self.notifications; - organization.value = data.self.organization; - role.value = data.self.role; - urls.value = data.urls; - username.value = data.self.username; - console.log("loaded user data", data); - } catch (e) { - error.value = e instanceof Error ? e.message : "an error ocurred"; - console.error("Error fetching user:", e); - } finally { - loading.value = false; - } - } - - async function isAuthenticated(): boolean { - console.log("pretend check user auth"); - return true; - } - return { - // State - display_name, - error, - initials, - loading, - notification_counts, - notifications, - organization, - role, - urls, - username, - // Actions - fetchUser, - isAuthenticated, - }; -}); diff --git a/ts/store/users.ts b/ts/store/users.ts index 33bfb12c..5482fd90 100644 --- a/ts/store/users.ts +++ b/ts/store/users.ts @@ -2,11 +2,11 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { User } from "../types"; import { SSEManager } from "../SSEManager"; -import { useUserStore } from "./user"; +import { useSessionStore } from "./session"; export const useUsersStore = defineStore("users", () => { // State - const _byID = ref>(new Map()); + const _byID = ref>(new Map()); const all = ref(null); const loading = ref(false); const error = ref(null); @@ -18,14 +18,14 @@ export const useUsersStore = defineStore("users", () => { } }); // Actions - function byID(id: int) { + function byID(id: number) { const result = _byID.value.get(id); console.log("user", id, result); return result; } async function fetchAll(): Promise { - const userStore = useUserStore(); - if (userStore.urls == null) { + const session = useSessionStore(); + if (session.urls == null) { throw new Error("can't fetch without user URL data"); } @@ -36,7 +36,7 @@ export const useUsersStore = defineStore("users", () => { params.append("sort", "-created"); //if (typeFilter.value) params.append("type", typeFilter.value); - const response = await fetch(`${userStore.urls.api.users}?${params}`); + const response = await fetch(`${session.urls.api.user}?${params}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -52,16 +52,16 @@ export const useUsersStore = defineStore("users", () => { throw err; } } - async function fetchOne(id: int) { - const userStore = useUserStore(); - if (userStore.urls == null) { + async function fetchOne(id: number) { + 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 response = await fetch(`${userStore.urls.api.user}/${id}`); + const response = await fetch(`${session.urls.api.user}/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); diff --git a/ts/types.ts b/ts/types.ts index 333943b9..533133ac 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -1,7 +1,22 @@ +import type { Map as MapLibreMap } from "maplibre-gl"; + export interface Address { + country: string; locality: string; number: string; + postal_code: string; + raw: string; + region: string; street: string; + unit: string; +} +export interface Bounds { + min: Location; + max: Location; +} +export interface Changes { + updated: string[]; + unchanged: string[]; } export interface Communication { @@ -10,24 +25,240 @@ export interface Communication { public_report: PublicReport | null; type: string; } -export interface Point { - lat: Number; - lng: Number; +export interface Contact { + has_email: boolean; + has_phone: boolean; + name?: string; } -export interface Bounds { - min: Point; - max: Point; +export interface CSVPoolDetailCount { + existing: number; + new: number; + outside: number; +} +export interface CSVPoolError { + column: number; + line: number; + message: string; +} +export interface CSVPoolDetail { + count: CSVPoolDetailCount; + errors: CSVPoolError[]; + pools: UploadPoolRow[]; +} +export interface Exif { + created: string; + make: string; + model: string; +} +export interface Followup { + description: string; + id: number; + title: string; +} +export interface Image { + distance_from_reporter_meters?: number; + exif: Exif; + exif_make: string; + exif_model: string; + exif_datetime: string; + location?: Location; + report_id: number; + url_content: string; + uuid: string; +} +export interface Lead { + description: string; + id: number; + title: string; +} +export interface Location { + lat: number; + lng: number; +} +export interface LogEntry { + created: string; + id: number; + message: string; + report_id: number; + type: string; + user_id: number; +} +export interface MapClickEvent { + location: Location; + map: MapLibreMap; + point: Point; } export interface Marker { color: string; draggable: boolean; id: string; - location: Point; + location: Location; } +export interface Nuisance { + additional_info: string; + duration: string; + is_location_backyard: boolean; + is_location_frontyard: boolean; + is_location_garden: boolean; + is_location_other: boolean; + is_location_pool: boolean; + source_container: boolean; + source_description: string; + source_gutter: boolean; + source_stagnant: boolean; + time_of_day_day: boolean; + time_of_day_early: boolean; + time_of_day_evening: boolean; + time_of_day_night: boolean; +} +export interface Organization { + id: number; + service_area?: Bounds; +} +export interface Point { + x: number; + y: number; +} +export interface Pool { + condition: string; + id: number; + location: Location; + site: Site; +} export interface PublicReport { + address: Address; + address_raw: string; created: string; + images: Image[]; + location?: Location; + log: LogEntry[]; + nuisance?: Nuisance; + public_id: string; + reporter: Contact; + status: string; + type: string; + water?: Water; +} +export interface Signal { + address?: Address; + addressed?: string; + addressor?: number; + created: string; + creator: number; + id: number; + location: Location; + pool?: Pool; + report?: PublicReport; + species?: string; type: string; } +export interface ReviewTask { + address: Address; + addressed?: string; + addressor?: User; + created: string; + creator: User; + pool?: ReviewTaskPool; + id: number; +} +export interface ReviewTaskPool { + condition: string; + location: Location; + owner: Contact; + site: Site; +} -export interface Signal {} +export interface Site { + address: Address; + created: string; + creator_id: number; + file_id: number; + id: number; + location: Location; + notes: string; + organization_id: number; + owner?: Contact; + parcel_id?: number; + resident?: Contact; + resident_owned: boolean; + tags: Map; + version: number; +} +export interface Upload { + created: string; + filename: string; + id: number; + recordcount: number; + status: string; + type: string; + csv_pool?: CSVPoolDetail; +} +export interface UploadPoolRow { + address: Address; + condition: string; + errors: UploadPoolError[]; + status: string; + tags: Map; +} +export interface UploadPoolError { + column: number; + line: number; + message: string; +} + +export interface URLs { + api: URLsAPI; + tegola: string; + tile: string; +} +// Define interfaces matching your Go structs +interface URLsAPI { + avatar: string; + communication: string; + publicreport_message: string; + review_task: string; + signal: string; + upload: string; + user: string; +} + +export interface User { + active: boolean; + avatar: string; + display_name: string; + id: number; + initials: string; + notifications: Notification[]; + notification_counts: UserNotificationCounts; + organization: Organization; + role: string; + tags: string[]; + uri: string; + username: string; +} +export interface UserNotificationCounts { + communication: number; + home: number; + review: number; +} +export interface UserResponse { + self: User; + urls: URLs; +} +export interface Water { + access_comments: string; + access_gate: boolean; + access_fence: boolean; + access_locked: boolean; + access_dog: boolean; + access_other: boolean; + comments: string; + has_adult: boolean; + has_backyard_permission: boolean; + has_larvae: boolean; + has_pupae: boolean; + is_reporter_confidential: boolean; + is_reporter_owner: boolean; + owner: Contact; +} diff --git a/ts/view/Communication.vue b/ts/view/Communication.vue index 9054b685..df245d8c 100644 --- a/ts/view/Communication.vue +++ b/ts/view/Communication.vue @@ -27,8 +27,7 @@ :mapBounds="mapBounds || undefined" :mapMarkers="mapMarkers" :selectedCommunication="selectedCommunication" - :user="user" - @viewImage="openPhotoViewer" + @viewImage="openImageViewer" /> - { fetchCommunications(); }); // Refs -const currentPhotoIndex = ref(0); +const currentImageIndex = ref(0); const error = ref(null); const loading = ref(true); const mapBounds = ref(null); const mapMarkers = ref([]); const selectedId = ref(null); -const showPhotoModal = ref(false); +const showImageModal = ref(false); const toastMessage = ref(""); const toastShow = ref(false); const toastTitle = ref(""); -const currentPhoto = computed(() => { +const currentImage = computed(() => { const comm = selectedCommunication.value; - return comm.public_report?.images[currentPhotoIndex] ?? null; + return comm?.public_report?.images[currentImageIndex.value] ?? null; }); const currentImages = computed(() => { const comm = selectedCommunication.value; @@ -99,16 +98,18 @@ const currentImages = computed(() => { } return comm.public_report.images ?? []; }); -const selectedCommunication = computed(() => { - if (selectedId.value == null) { - return null; - } - if (communication.all == null) { - return null; - } - const result = communication.all.find((c) => c.id == selectedId.value); - return result; -}); +const selectedCommunication = computed( + (): Communication | null => { + if (selectedId.value == null) { + return null; + } + if (communication.all == null) { + return null; + } + const result = communication.all.find((c) => c.id == selectedId.value); + return result || null; + }, +); const handleDeselect = (id: string) => { selectedId.value = null; updateMap(); @@ -119,24 +120,15 @@ const handleSelect = (id: string) => { }; async function fetchCommunications() { await communication.fetchAll(); - // if we already had something selected, reset it using the new data - if (selectedCommunication.value) { - const matching = communication.all.filter((c) => { - return c.id === selectedCommunication.value.id; - }); - if (matching.length > 0) { - selectedCommunication.value = matching[0]; - } - } } function imageNext() { - currentPhotoIndex.value = Math.min( + currentImageIndex.value = Math.min( currentImages.value.length - 1, - currentPhotoIndex.value + 1, + currentImageIndex.value + 1, ); } function imagePrevious() { - currentPhotoIndex.value = Math.max(0, currentPhotoIndex.value - 1); + currentImageIndex.value = Math.max(0, currentImageIndex.value - 1); } async function loadFromAPI() { loading.value = true; @@ -144,19 +136,22 @@ async function loadFromAPI() { try { await Promise.all([fetchCommunications()]); } catch (err) { - error.value = err.message; + error.value = err instanceof Error ? err.message : "fetch error"; console.error("Error loading data:", err); } finally { loading.value = false; } } -function openPhotoViewer(index) { - currentPhotoIndex.value = index; - showPhotoModal.value = true; +function openImageViewer(index: number) { + currentImageIndex.value = index; + showImageModal.value = true; } async function markInvalid() { + if (selectedCommunication.value == null) { + return; + } console.log("Marking report as invalid:", selectedCommunication.value.id); const payload = { reportID: selectedCommunication.value.id, @@ -178,6 +173,9 @@ async function markInvalid() { } async function markSignal() { + if (selectedCommunication.value == null) { + return; + } console.log("Marking report as signal:", selectedCommunication.value.id); try { const report_id = selectedCommunication.value.id; @@ -201,12 +199,15 @@ async function markSignal() { ); await fetchCommunications(); } catch (err) { - error.value = err.message; + error.value = err instanceof Error ? err.message : "fetch error"; console.error("Error creating lead:", err); } } function removeCurrentFromList() { + if (communication.all == null) { + return; + } const index = communication.all.findIndex((c) => c.id === selectedId.value); if (index > -1) { communication.all.splice(index, 1); @@ -220,14 +221,15 @@ function removeCurrentFromList() { } async function sendMessage(message: string) { if (!message.trim()) return; - - console.log("Sending message reporter:", message.value); + if (selectedCommunication.value == null) return; + if (session.urls == null) return; + console.log("Sending message reporter:", message); const payload = { - message: message.value, + message: message, reportID: selectedCommunication.value.id, }; - const response = await fetch(user.urls.api.publicreport_message, { + const response = await fetch(session.urls?.api.publicreport_message, { method: "POST", headers: { "Content-Type": "application/json", @@ -241,11 +243,10 @@ async function sendMessage(message: string) { showNotification( "Message Sent", - `Message successfully sent to ${selectedCommunication.value.public_report.reporter.name}`, + `Message successfully sent to ${selectedCommunication.value.public_report?.reporter.name}`, ); - messageText.value = ""; } -function showNotification(title, message) { +function showNotification(title: string, message: string) { toastTitle.value = title; toastMessage.value = message; toastShow.value = true; @@ -268,35 +269,26 @@ function updateMap() { color: "#FF0000", draggable: false, id: String(Date.now()), - location: { - lng: loc.longitude, - lat: loc.latitude, - }, + location: loc, }, ]; console.log("markers now", mapMarkers.value); - let min = { lat: loc.latitude, lng: loc.longitude }; - let max = { lat: loc.latitude, lng: loc.longitude }; + let min = loc; + let max = loc; - for (const i of selectedCommunication.value.public_report.images) { - if ( - i.location != null && - i.location.latitude != 0 && - i.location.longitude != 0 - ) { + for (const i of selectedCommunication.value?.public_report?.images ?? []) { + if (i.location != null && i.location.lat != 0 && i.location.lng != 0) { mapMarkers.value.push({ color: "#00FF00", draggable: false, - location: { - lat: i.location.latitude, - lng: i.location.longitude, - }, + id: new Date().toISOString(), + location: i.location, }); - min.lat = Math.min(min.lat, i.location.latitude); - min.lng = Math.min(min.lng, i.location.longitude); - max.lat = Math.max(max.lat, i.location.latitude); - max.lng = Math.max(max.lng, i.location.longitude); + min.lat = Math.min(min.lat, i.location.lat); + min.lng = Math.min(min.lng, i.location.lng); + max.lat = Math.max(max.lat, i.location.lat); + max.lng = Math.max(max.lng, i.location.lng); } } @@ -311,9 +303,6 @@ function updateMap() { }, }; } -function onFilterChange(filters) { - console.log("Filters changed"); -} // Lifecycle hooks onMounted(async () => { await loadFromAPI(); diff --git a/ts/view/Home.vue b/ts/view/Home.vue index 78bffc1c..f5912ed5 100644 --- a/ts/view/Home.vue +++ b/ts/view/Home.vue @@ -120,17 +120,15 @@

Mosquito Activity Heatmap

-

+

No service area for this organization yet

-
@@ -152,17 +150,6 @@ Action - - - {{ formatTimeRelative(sr.date) }} - Service Request - {{ sr.location }} - Completed - - View - - - @@ -173,7 +160,9 @@ + diff --git a/ts/view/configuration/Upload.vue b/ts/view/configuration/Upload.vue index d47282d0..035461f6 100644 --- a/ts/view/configuration/Upload.vue +++ b/ts/view/configuration/Upload.vue @@ -143,7 +143,7 @@ upload.status }} - {{ upload.record_count }} entries + {{ upload.recordcount }} entries @@ -160,9 +160,10 @@ import { computed, onMounted } from "vue"; import TimeRelative from "@/components/TimeRelative.vue"; import { useUploadStore } from "@/store/upload"; +import { Upload } from "@/types"; const uploadStore = useUploadStore(); -const uploads = computed(() => { +const uploads = computed((): Upload[] | null => { return uploadStore.all; }); onMounted(() => { diff --git a/ts/view/configuration/UploadDetail.vue b/ts/view/configuration/UploadDetail.vue index 8de1d8d5..60bd7046 100644 --- a/ts/view/configuration/UploadDetail.vue +++ b/ts/view/configuration/UploadDetail.vue @@ -100,238 +100,219 @@ tr.has-error {