diff --git a/api/user.go b/api/user.go index 532f1568..9ce1b2d5 100644 --- a/api/user.go +++ b/api/user.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/html" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" @@ -12,10 +13,12 @@ import ( type contentURLAPI struct { Communication string `json:"communication"` PublicreportMessage string `json:"publicreport_message"` + Signal string `json:"signal"` } type contentURLs struct { API contentURLAPI `json:"api"` Tegola string `json:"tegola"` + Tile string `json:"tile"` } type contentUserSelf struct { Self platform.User `json:"self"` @@ -27,6 +30,11 @@ func getUserSelf(ctx context.Context, r *http.Request, user platform.User, query if err != nil { return nil, nhttp.NewError("get notifications: %w", err) } + org, err := platform.OrganizationByID(ctx, int(user.Organization.ID)) + if err != nil { + return nil, nhttp.NewError("get org: %w", err) + } + user.Organization = *org user.NotificationCounts = *counts urls := html.NewContentURL() return &contentUserSelf{ @@ -35,8 +43,10 @@ func getUserSelf(ctx context.Context, r *http.Request, user platform.User, query API: contentURLAPI{ Communication: urls.API.Communication, PublicreportMessage: urls.API.Publicreport.Message, + Signal: config.MakeURLNidus("/api/signal"), }, Tegola: urls.Tegola, + Tile: config.MakeURLNidus("/api/tile/{z}/{y}/{x}"), }, }, nil } diff --git a/html/url.go b/html/url.go index b1f65978..1064b39b 100644 --- a/html/url.go +++ b/html/url.go @@ -40,6 +40,7 @@ type contentURLAPI struct { func newContentURLAPI() contentURLAPI { return contentURLAPI{ Communication: config.MakeURLNidus("/api/communication"), + Publicreport: newContentURLAPIPublicreport(), } } diff --git a/platform/organization.go b/platform/organization.go index a8a3aeea..996d7875 100644 --- a/platform/organization.go +++ b/platform/organization.go @@ -53,7 +53,9 @@ func (o Organization) IsCatchall() bool { } func (o Organization) MarshalJSON() ([]byte, error) { to_marshal := map[string]any{} + to_marshal["id"] = o.ID to_marshal["name"] = o.Name() + to_marshal["service_area"] = o.ServiceArea return json.Marshal(to_marshal) } func (o Organization) Name() string { diff --git a/sync/routes.go b/sync/routes.go index 6e0f75bd..d3e7e85e 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -43,6 +43,7 @@ func Router() chi.Router { r.Method("GET", "/", authenticatedHandler(getRoot)) r.Method("GET", "/communication", authenticatedHandler(getRoot)) r.Method("GET", "/intelligence", authenticatedHandler(getRoot)) + r.Method("GET", "/planning", authenticatedHandler(getRoot)) r.Method("GET", "/admin", authenticatedHandler(getAdminDash)) r.Method("GET", "/cell/{cell}", authenticatedHandler(getCellDetails)) @@ -71,7 +72,6 @@ func Router() chi.Router { r.Method("GET", "/oauth/refresh", authenticatedHandler(getOAuthRefresh)) r.Method("GET", "/operations", authenticatedHandler(getOperationsRoot)) r.Method("GET", "/parcel", authenticatedHandler(getParcel)) - r.Method("GET", "/planning", authenticatedHandler(getPlanningRoot)) r.Method("GET", "/pool", authenticatedHandler(getPoolList)) r.Method("GET", "/pool/create", authenticatedHandler(getPoolCreate)) r.Method("GET", "/pool/{id}", authenticatedHandler(getPoolByID)) diff --git a/ts/components/MapProxiedArcgisTile.vue b/ts/components/MapProxiedArcgisTile.vue new file mode 100644 index 00000000..a844371b --- /dev/null +++ b/ts/components/MapProxiedArcgisTile.vue @@ -0,0 +1,201 @@ + + + + + + + diff --git a/ts/components/PlanningColumnAction.vue b/ts/components/PlanningColumnAction.vue new file mode 100644 index 00000000..e9c2a60b --- /dev/null +++ b/ts/components/PlanningColumnAction.vue @@ -0,0 +1,81 @@ + + + + Transformation Tools + + + Signal → Lead + + Create New Lead from Selection + + + Creating... + + + + Add Signals to Existing Lead + + + Mark Signal as Addressed + + + + + + + Lead → Field Assignment + + Create Proposed Assignment + + + Add Leads to Existing Assignment + + + Split Lead + + + + + + + Assignment Controls + Set Priority + + Estimate Effort + + + Send to Operations + + + + + + diff --git a/ts/components/PlanningColumnDetail.vue b/ts/components/PlanningColumnDetail.vue new file mode 100644 index 00000000..72e58209 --- /dev/null +++ b/ts/components/PlanningColumnDetail.vue @@ -0,0 +1,174 @@ + + + + + Active Investigation Workbench + + + + + + + + + + + Selected Signals + + {{ selectedSignals.length }} Signal{{ + selectedSignals.length !== 1 ? "s" : "" + }} + Selected + + + + Click signals from the left panel to select them + + + + + + + + + + + + Pool + Nuisance + Water + + + + + {{ shortAddress(signal.address) }} + + + + + + Clear Selection + + + + + + + + + + + + + + diff --git a/ts/components/PlanningColumnList.vue b/ts/components/PlanningColumnList.vue new file mode 100644 index 00000000..cee28720 --- /dev/null +++ b/ts/components/PlanningColumnList.vue @@ -0,0 +1,229 @@ + + + + + Incoming Signals & Leads + + + + + + Error: {{ error }} + + Retry + + + + + + Species + + All Species + Aedes aegypti + Aedes albopictus + Culex pipiens + Culex tarsalis + + + Signal Type + + All Types + Public Report + Trap Spike + Surveillance Observation + Residual Expiring + Plan Follow-Up + + + Sort By + + Newest First + Highest Priority + Most Signals Linked + Strongest Species Signal + + + + + + + + + Loading... + + + + + + Signals + + {{ selectedSignals.length }} + + + + + No signals found + + + + {{ signal.title }} + + {{ shortAddress(signal.address) }} + + {{ signal.description }} + + {{ signal.badge }} + + + + + + + + + + Mosquito Control Plan Follow-Ups + + + No plan follow-ups + + + + {{ followup.title }} + {{ followup.description }} + Plan + + + + + + + + Existing Leads + + + No existing leads + + + + {{ lead.title }} + {{ lead.description }} + + + + + + + diff --git a/ts/components/layout/ThreeColumn.vue b/ts/components/layout/ThreeColumn.vue index 2e6910d3..4ac17e9f 100644 --- a/ts/components/layout/ThreeColumn.vue +++ b/ts/components/layout/ThreeColumn.vue @@ -1,16 +1,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/ts/store/signal.ts b/ts/store/signal.ts new file mode 100644 index 00000000..638b6761 --- /dev/null +++ b/ts/store/signal.ts @@ -0,0 +1,52 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import { Signal } from "../types"; +import { SSEManager } from "../SSEManager"; +import { useUserStore } from "./user"; + +export const useSignalStore = defineStore("signal", () => { + // State + const all = ref(null); + const loading = ref(false); + const error = ref(null); + + // Subscription + SSEManager.subscribe("*", (e) => { + if (e.resource.startsWith("signal")) { + fetchAll(); + } + }); + // Actions + async function fetchAll() { + const userStore = useUserStore(); + if (userStore.urls == null) { + throw new Error("can't fetch without user URL data"); + } + + loading.value = true; + error.value = null; + try { + const params = new URLSearchParams(); + params.append("sort", "-created"); + //if (typeFilter.value) params.append("type", typeFilter.value); + + const response = await fetch(`${userStore.urls.api.signal}?${params}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + all.value = data.signals; + } catch (err) { + console.error("Error loading signals:", err); + throw err; + } + } + + return { + // State + all, + // Actions + fetchAll, + }; +}); diff --git a/ts/view/Planning.vue b/ts/view/Planning.vue index 8f3eab3d..009e9dd3 100644 --- a/ts/view/Planning.vue +++ b/ts/view/Planning.vue @@ -1,7 +1,14 @@ + + - - - + + Daily Planning Workbench @@ -10,401 +17,59 @@ right. - - - - - - - - Incoming Signals & Leads - - - - - - Error: {{ error }} - - Retry - - - - - - Species - - All Species - Aedes aegypti - Aedes albopictus - Culex pipiens - Culex tarsalis - - - Signal Type - - All Types - Public Report - Trap Spike - Surveillance Observation - Residual Expiring - Plan Follow-Up - - - Sort By - - Newest First - Highest Priority - Most Signals Linked - Strongest Species Signal - - - - - - - - - Loading... - - - - - - - Signals - - {{ selectedSignals.length }} - - - - - No signals found - - - - {{ signal.title }} - - {{ shortAddress(signal.address) }} - - {{ signal.description }} - - {{ signal.badge }} - - - - - - - - - - Mosquito Control Plan Follow-Ups - - - - No plan follow-ups - - - - {{ followup.title }} - {{ followup.description }} - Plan - - - - - - - - Existing Leads - - - No existing leads - - - - {{ lead.title }} - {{ lead.description }} - - - - - - - - - - - Active Investigation Workbench - - - - - - - - - - - Selected Signals - - {{ selectedSignals.length }} Signal{{ - selectedSignals.length !== 1 ? "s" : "" - }} - Selected - - - - Click signals from the left panel to select them - - - - - - - - - - - - Pool - Nuisance - Water - - - - - {{ shortAddress(signal.address) }} - - - - - - Clear Selection - - - - - - - - - - - - - - - - - - - Transformation Tools - - - - Signal → Lead - - Create New Lead from Selection - - - Creating... - - - - Add Signals to Existing Lead - - - Mark Signal as Addressed - - - - - - - Lead → Field Assignment - - Create Proposed Assignment - - - Add Leads to Existing Assignment - - - Split Lead - - - - - - - Assignment Controls - - Set Priority - - - Estimate Effort - - - Send to Operations - - - - - - - + + + + + + + + + + + - -