diff --git a/dispatch/resources/queries/user/organization/fetch-agent-locations.graphql b/dispatch/resources/queries/user/organization/fetch-agent-locations.graphql new file mode 100644 index 00000000..52e72100 --- /dev/null +++ b/dispatch/resources/queries/user/organization/fetch-agent-locations.graphql @@ -0,0 +1,26 @@ +query OrganizationAgentLocations($agentId: ID!) { + user { + organization { + agent(agentId: $agentId) { + id + name + user { + phone + } + places { + id + name + description + lat + lng + createdAt + } + locations { + id + position + createdAt + } + } + } + } +} diff --git a/dispatch/resources/queries/user/organization/fetch-agent-performance.graphql b/dispatch/resources/queries/user/organization/fetch-agent-performance.graphql new file mode 100644 index 00000000..d41ca147 --- /dev/null +++ b/dispatch/resources/queries/user/organization/fetch-agent-performance.graphql @@ -0,0 +1,23 @@ +query OrganizationPerformance($start: Date!, $end: Date!) { + user { + organization { + performance(start: $start, end: $end) { + id + name + places { + id + name + description + lat + lng + createdAt + } + locations { + id + position + createdAt + } + } + } + } +} diff --git a/dispatch/resources/schema.graphql b/dispatch/resources/schema.graphql index 3d37a8e5..1732538b 100644 --- a/dispatch/resources/schema.graphql +++ b/dispatch/resources/schema.graphql @@ -68,6 +68,7 @@ type Organization { vehicles: [Vehicle] plans: [Plan] plan(planId: ID!): Plan + performance(start: Date!, end: Date!): [Agent] } type Agent { @@ -75,6 +76,7 @@ type Agent { name: String! user: User location: Location + locations: [Location] places: [Place] place(placeId: ID!, filters: TaskFilters): Place tasks(filters: TaskFilters): [Task] @@ -109,6 +111,7 @@ type Place { lat: Float! lng: Float! tasks: [Task] + createdAt: Date } type Stop { diff --git a/dispatch/scripts/find-routes.mjs b/dispatch/scripts/find-routes.mjs new file mode 100644 index 00000000..61aaa7de --- /dev/null +++ b/dispatch/scripts/find-routes.mjs @@ -0,0 +1,155 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +const user = await prisma.user.findUnique({ + where: { email: "rodrigo@ambito.app" }, + include: { + organization: { + include: { + places: { + include: { + agent: true, + }, + }, + agents: { + include: { + locations: { + orderBy: { + createdAt: "asc", + }, + }, + }, + }, + }, + }, + }, +}); + +const places = user.organization.places; + +// group places by created day +const places_by_day = places.reduce((acc, place) => { + const date = place.createdAt.toISOString().slice(0, 10); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(place); + return acc; +}, {}); + +// group places by day by agent +const places_by_day_by_agent = Object.entries(places_by_day).map( + ([date, places]) => [ + new Date(date).toLocaleDateString("es-AR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }), + places.reduce((acc, place) => { + const agent = place.agent; + if (!agent) { + return acc; + } + if (!acc[agent.name]) { + acc[agent.name] = []; + } + acc[agent.name].push(place.name); + return acc; + }, {}), + ] +); + +function haversine(lat1, lon1, lat2, lon2) { + function toRad(degree) { + return (degree * Math.PI) / 180; + } + + const R = 6371; // Earth's radius in kilometers + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; +} + +function totalDistanceTraveled(locations) { + let totalDistance = 0; + + for (let i = 0; i < locations.length - 1; i++) { + const loc1 = locations[i]; + const loc2 = locations[i + 1]; + + const distance = haversine( + loc1.latitude, + loc1.longitude, + loc2.latitude, + loc2.longitude + ); + totalDistance += distance; + } + + return totalDistance; +} + +// const places_by_day_by_agent_with_distance = places_by_day_by_agent.map( +// ([date, places_by_agent]) => [ +// date, +// Object.entries(places_by_agent).map(([agent_name, places]) => [ +// agent_name, +// places, +// user.organization.agents +// .find((agent) => { +// // console.log(agent); +// return agent.name === agent_name; +// }) +// .locations.filter( +// (location) => +// new Date(location.createdAt).toLocaleDateString("es-AR", { +// weekday: "long", +// year: "numeric", +// month: "long", +// day: "numeric", +// }) === date +// ) +// .map((location) => { +// // console.log(location); +// return location.position; +// }).length, +// ]), +// ] +// ); + +const agent_locations_by_day = user.organization.agents.map((agent) => { + const locations_by_day = agent.locations.reduce((acc, location) => { + const date = location.createdAt.toISOString().slice(0, 10); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(location); + return acc; + }, {}); + + return [ + agent.name, + Object.entries(locations_by_day).map(([date, locations]) => [ + new Date(date).toLocaleDateString("es-AR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }), + locations.length, + totalDistanceTraveled(locations.map((location) => location.position)), + ]), + ]; +}); + +console.log(JSON.stringify(agent_locations_by_day)); diff --git a/dispatch/src/api/lib/apollo.cljs b/dispatch/src/api/lib/apollo.cljs index 3243f3cb..698e43d9 100644 --- a/dispatch/src/api/lib/apollo.cljs +++ b/dispatch/src/api/lib/apollo.cljs @@ -63,7 +63,8 @@ :organization #() :agent #()} :Organization - {:agents agent/fetch-organization-agents + {:performance agent/fetch-organization-performance + :agents agent/fetch-organization-agents :agent agent/fetch-organization-agent :places place/fetch-organization-places :place place/fetch-organization-place diff --git a/dispatch/src/api/models/agent.cljs b/dispatch/src/api/models/agent.cljs index 941727f8..6713e305 100644 --- a/dispatch/src/api/models/agent.cljs +++ b/dispatch/src/api/models/agent.cljs @@ -33,7 +33,7 @@ (.forEach agents (fn [^js agent] - (set! (.-location agent) (first (.-locations agent))))) + (set! (.-location agent) (last (.-locations agent))))) ;; this moves null values to the end of the list (sort-by #(some-> ^js % .-location .-createdAt) > agents))) @@ -47,11 +47,28 @@ {:where {:id agentId} :include {:user true - :locations {:take 1 - :orderBy {:createdAt "desc"}} + :places {:orderBy {:createdAt "asc"}} + :locations {:orderBy {:createdAt "asc"}} :tasks {:where (filters/task filters) :orderBy {:startAt "asc"} :include {:stops {:include {:place true}}}}}}}}}}) ^js agent (first (.. result -organization -agents))] - (set! (.-location agent) (first (.. agent -locations))) + (set! (.-location agent) (last (.. agent -locations))) agent)) + +(defn fetch-organization-performance [^js context {:keys [start end]}] + (p/let [^js user (active-user + context + {:include + {:organization + {:include + {:agents + {:include + {:user true + :places + {:where {:createdAt {:gte start :lte end}} + :orderBy {:createdAt "asc"}} + :locations + {:where {:createdAt {:gte start :lte end}} + :orderBy {:createdAt "asc"}}}}}}}})] + (.. user -organization -agents))) diff --git a/dispatch/src/api/resolvers/agent.cljs b/dispatch/src/api/resolvers/agent.cljs index 0d5b3a58..13ee313c 100644 --- a/dispatch/src/api/resolvers/agent.cljs +++ b/dispatch/src/api/resolvers/agent.cljs @@ -13,3 +13,7 @@ (defn fetch-organization-agent [_ args context _] (agent/fetch-organization-agent context (->clj args))) + +(defn fetch-organization-performance + [_ args context _] + (agent/fetch-organization-performance context (->clj args))) diff --git a/dispatch/src/api/resolvers/place.cljs b/dispatch/src/api/resolvers/place.cljs index 4b92af85..9101ccd4 100644 --- a/dispatch/src/api/resolvers/place.cljs +++ b/dispatch/src/api/resolvers/place.cljs @@ -15,8 +15,9 @@ (place/fetch-organization-place context (->clj args))) (defn fetch-agent-places - [_ _ context _] - (place/fetch-agent-places context)) + [^js parent _ context _] + (or (.. parent -places) + (place/fetch-agent-places context))) (defn fetch-agent-place [_ args context _] diff --git a/dispatch/src/ui/events.cljs b/dispatch/src/ui/events.cljs index af9db247..863e97cc 100644 --- a/dispatch/src/ui/events.cljs +++ b/dispatch/src/ui/events.cljs @@ -39,6 +39,13 @@ [trim-v] (assoc-key :map)) +(rf/reg-event-db + :map/locations + [trim-v] + (fn [db [v]] + (assoc-in + db [:map :locations] v))) + (rf/reg-event-db :layout/toggle-nav (fn [db] diff --git a/dispatch/src/ui/hooks/use_map.cljs b/dispatch/src/ui/hooks/use_map.cljs index 63addc9d..4ef69021 100644 --- a/dispatch/src/ui/hooks/use_map.cljs +++ b/dispatch/src/ui/hooks/use_map.cljs @@ -8,7 +8,7 @@ [ui.lib.google.maps.core :refer (init-api)] [ui.lib.google.maps.polyline :refer (set-polylines clear-polylines decode-polyline)] [ui.lib.google.maps.marker :refer (set-markers clear-markers)] - [ui.lib.google.maps.overlay :refer (set-overlays clear-overlays)] + [ui.lib.google.maps.overlay :refer (update-overlays)] [ui.utils.location :refer (position-to-lat-lng)])) (def ^:private !el (atom nil)) @@ -60,16 +60,24 @@ (useEffect (fn [] - (if @!map (let [polylines (when (seq paths) (set-polylines @!map paths)) - markers (when (seq points) (set-markers @!map points)) - overlays (when (or (seq locations) position) - (set-overlays @!map (remove nil? (conj locations position))))] - #(do - (clear-polylines polylines) - (clear-markers markers) - (clear-overlays overlays))) + (if @!map (let [polylines (when (seq paths) (set-polylines @!map paths))] + #(clear-polylines polylines)) #())) - #js[@!map paths points locations position]) + #js[@!map (js/JSON.stringify paths)]) + + (useEffect + (fn [] + (if @!map (let [markers (when (seq points) (set-markers @!map points))] + #(clear-markers markers)) + #())) + #js[@!map (js/JSON.stringify points)]) + + (useEffect + (fn [] + (when @!map + (update-overlays @!map (remove nil? (conj locations position))) + #())) + #js[@!map locations position]) {:ref map-ref :center center})) @@ -96,9 +104,10 @@ (filterv some? (mapv - (fn [{:keys [lat lng name]}] + (fn [{:keys [lat lng name color]}] (when (and lat lng) {:title (or name "???") + :color color :position {:lat lat :lng lng}})) places)) :locations diff --git a/dispatch/src/ui/lib/google/maps/directions.cljs b/dispatch/src/ui/lib/google/maps/directions.cljs index cfe10f22..7f7baf9c 100644 --- a/dispatch/src/ui/lib/google/maps/directions.cljs +++ b/dispatch/src/ui/lib/google/maps/directions.cljs @@ -7,19 +7,29 @@ (defn- create-service [] (js/google.maps.DirectionsService.)) -(defn- create-route-request [places] - (let [stops (map (fn [%] {:location (->js %) :stopover true}) places)] - (->js {:origin (-> stops first :location ->js) - :destination (-> stops last :location ->js) - :waypoints (->> stops (drop-last 1) ->js) - ;; :optimizeWaypoints true - :travelMode "DRIVING"}))) +(defn- create-route-request [places options] + (let [stops (map (fn [%] {:location % :stopover true}) places)] + (->js (merge {:origin (-> stops first :location ->js) + :destination (-> stops last :location ->js) + :waypoints (->> stops (drop-last 1) ->js) + :travelMode "DRIVING"} + options)))) (defn calc-route [places] (js/Promise. (fn [resolve _] (let [^js service @!service - request (create-route-request places)] + request (create-route-request places {})] + (.route service request + (fn [response status] + (when (= status "OK") + (resolve (parse-route response))))))))) + +(defn calc-optimized-route [places] + (js/Promise. + (fn [resolve _] + (let [^js service @!service + request (create-route-request places {:optimizeWaypoints true})] (.route service request (fn [response status] (when (= status "OK") diff --git a/dispatch/src/ui/lib/google/maps/marker.cljs b/dispatch/src/ui/lib/google/maps/marker.cljs index 89e2ac31..67db0859 100644 --- a/dispatch/src/ui/lib/google/maps/marker.cljs +++ b/dispatch/src/ui/lib/google/maps/marker.cljs @@ -1,9 +1,10 @@ (ns ui.lib.google.maps.marker - (:require ["@googlemaps/markerclusterer" :refer (MarkerClusterer)] - [reagent.dom.server :refer (render-to-string)] - [cljs-bean.core :refer (->js)])) + (:require + ;; ["@googlemaps/markerclusterer" :refer (MarkerClusterer NoopAlgorithm)] + [reagent.dom.server :refer (render-to-string)] + [cljs-bean.core :refer (->js)])) -(def !clusterer (atom nil)) +;; (def !clusterer (atom nil)) (defn create-marker [options] (js/google.maps.Marker. (->js options))) @@ -17,6 +18,8 @@ :width 30 :height 30 :viewBox "0 0 20 20" + :stroke-width 0.5 + :stroke "black" :fill color} [:path {:fill-rule "evenodd" :d "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 NaN" @@ -38,30 +41,40 @@ (defn set-markers [^js gmap points] (let [markers (->js (mapv - (fn [[idx {:keys [position title]}]] + (fn [[idx {:keys [position title color]}]] (create-marker {:map gmap :zIndex idx :position position - :label {:text title + :label {:text " " ;; title :fontSize "0.875rem"} - :icon {:url (render-marker "#ef4444") + :icon {:url (render-marker (or color "#ef4444")) :scaledSize (js/google.maps.Size. 30 30) :labelOrigin (js/google.maps.Point. 15 -6)}})) (map-indexed vector points))) - ^js clusterer @!clusterer] - (if clusterer - (do - (.setMap clusterer gmap) - (.addMarkers clusterer markers)) - (reset! - !clusterer - (MarkerClusterer. - (->js {:map gmap - :markers markers - :renderer {:render render}})))) + ;; ^js clusterer @!clusterer + ] + (doseq [^js marker markers] + (.setMap marker gmap)) + ;; (if clusterer + ;; (do + ;; (.setMap clusterer gmap) + ;; (.addMarkers clusterer markers)) + ;; (reset! + ;; !clusterer + ;; (MarkerClusterer. + ;; (->js {:map gmap + ;; :markers markers + ;; :algorithm NoopAlgorithm + ;; :renderer {:render render}})))) markers)) -(defn clear-markers [_] - (when-let [^js clusterer @!clusterer] - (.clearMarkers clusterer))) +(defn clear-markers [markers] + (doseq [^js marker markers] + (.setMap marker nil)) + + ;; (when-let [^js clusterer @!clusterer] + ;; (.clearMarkers clusterer)) + ) + + diff --git a/dispatch/src/ui/lib/google/maps/overlay.cljs b/dispatch/src/ui/lib/google/maps/overlay.cljs index 3860c344..74a032ec 100644 --- a/dispatch/src/ui/lib/google/maps/overlay.cljs +++ b/dispatch/src/ui/lib/google/maps/overlay.cljs @@ -79,13 +79,25 @@ (defn clear-overlay [^js overlay] (.setMap overlay nil)) -(defn set-overlays [^js gmap locations] - (mapv (fn [{:keys [title position]}] - (let [overlay (create-overlay gmap)] - (.update overlay title position) - overlay)) - locations)) - (defn clear-overlays [overlays] (doseq [overlay overlays] (clear-overlay overlay))) + +(def !overlays (atom [])) + +(defn update-overlays [^js gmap locations] + (doseq [[idx overlay] (map-indexed vector @!overlays)] + (when (> idx (dec (count locations))) + (clear-overlay overlay))) + (mapv + (fn [[idx {:keys [title position]}]] + (let [^js existing-overlay (get @!overlays idx) + overlay (or existing-overlay + (create-overlay gmap))] + (.update overlay title position) + (if existing-overlay + (when-not (.. existing-overlay -map) + (.setMap existing-overlay gmap)) + (swap! !overlays conj overlay)) + overlay)) + (map-indexed vector locations))) diff --git a/dispatch/src/ui/lib/google/maps/polyline.cljs b/dispatch/src/ui/lib/google/maps/polyline.cljs index 79baf8b1..f2f7febf 100644 --- a/dispatch/src/ui/lib/google/maps/polyline.cljs +++ b/dispatch/src/ui/lib/google/maps/polyline.cljs @@ -34,7 +34,7 @@ (defn decode-polyline [encoded-polyline] (let [^js google (some-> js/window .-google) decode (when google (.. google -maps -geometry -encoding -decodePath))] - (when google + (when (and decode encoded-polyline) (->> (decode encoded-polyline) (mapv (fn [^js latlng] (let [lat (.lat latlng) diff --git a/dispatch/src/ui/views/organization/agent/core.cljs b/dispatch/src/ui/views/organization/agent/core.cljs index 99cdf67b..22cf3245 100644 --- a/dispatch/src/ui/views/organization/agent/core.cljs +++ b/dispatch/src/ui/views/organization/agent/core.cljs @@ -1,8 +1,12 @@ (ns ui.views.organization.agent.core (:require [ui.views.organization.agent.list :as list] [ui.views.organization.agent.detail :as detail] + [ui.views.organization.agent.locations :as locations] + [ui.views.organization.agent.performance :as performance] [ui.views.organization.agent.create :as create])) (def list-view list/view) +(def performance-view performance/view) (def detail-view detail/view) -(def create-view create/view) +(def locations-view locations/view) +(def create-view create/view) \ No newline at end of file diff --git a/dispatch/src/ui/views/organization/agent/locations.cljs b/dispatch/src/ui/views/organization/agent/locations.cljs new file mode 100644 index 00000000..d7e02a26 --- /dev/null +++ b/dispatch/src/ui/views/organization/agent/locations.cljs @@ -0,0 +1,128 @@ +(ns ui.views.organization.agent.locations + (:require ["react" :refer (useState useEffect)] + [re-frame.core :refer (dispatch)] + [shadow.resource :refer (inline)] + [common.utils.date :refer (parse-date)] + [ui.lib.apollo :refer (gql use-query)] + [ui.lib.router :refer (use-params)] + [ui.lib.google.maps.directions :refer (calc-route calc-optimized-route)] + [ui.utils.input :refer (debounce-cb)] + [ui.utils.date :as d] + [ui.utils.i18n :refer (tr)] + [ui.hooks.use-map :refer (use-map-items)] + [ui.components.layout.map :refer (map-layout)] + [ui.components.inputs.combobox :refer (combobox)])) + +(def FETCH_ORGANIZATION_AGENT_LOCATIONS (gql (inline "queries/user/organization/fetch-agent-locations.graphql"))) + +(defn connected-partition [n coll] + (when (seq coll) + (lazy-seq (cons (take n coll) (connected-partition n (drop (dec n) coll)))))) + +(defn view [] + (let [{agent-id :agent} (use-params) + query (use-query FETCH_ORGANIZATION_AGENT_LOCATIONS {:variables {:agentId agent-id}}) + {:keys [data loading]} query + {:keys [name locations places]} (some-> data :user :organization :agent) + grouped-locations (->> (group-by + (fn [location] (-> location :createdAt parse-date (d/format "yyyy-MM-dd"))) + locations) + (sort-by (fn [[date _]] + (js/Date. date)) <)) + [routes set-routes] (useState nil) + [optimized-routes set-optimized-routes] (useState nil) + [selected-date set-selected-date] (useState nil) + [current-location-index set-current-location-index] (useState 0) + selected-locations (->> grouped-locations + (filter (fn [[date _]] (= date selected-date))) + (map second) + first) + current-location (nth selected-locations current-location-index) + current-date-created-places (filter + (fn [place] (= (-> place :createdAt parse-date (d/format "yyyy-MM-dd")) + selected-date)) + places) + routes-total-travel-distance (->> routes (map #(->> % :legs (map :distance) (reduce +))) (reduce +)) + routes-total-travel-duration (->> routes (map #(->> % :legs (map :duration) (reduce +))) (reduce +)) + optimized-total-travel-distance (->> optimized-routes (map #(->> % :legs (map :distance) (reduce +))) (reduce +)) + optimized-total-travel-duration (->> optimized-routes (map #(->> % :legs (map :duration) (reduce +))) (reduce +))] + + (useEffect + (fn [] + (let [time-range-interval + (when selected-date + (js/setInterval + (fn [] + (set-current-location-index + (fn [current] + (if (= current (dec (count selected-locations))) + 0 + (inc current))))) + 50))] + (fn [] + (js/clearInterval time-range-interval)))) + (array selected-date)) + + (useEffect + (fn [] + (dispatch [:map/locations + [{:title (-> current-location :createdAt parse-date (d/format "hh:mm aaa")) + :position (:position current-location)}]]) + #()) + (array current-location-index)) + + (useEffect + (fn [] + (debounce-cb + (fn [] + (if (seq current-date-created-places) + (let [places-groups (connected-partition 25 current-date-created-places) + routes (mapv + (fn [places] + (calc-route places)) + places-groups) + optimized-routes (mapv + (fn [places] + (calc-optimized-route places)) + places-groups)] + (-> (js/Promise.all routes) + (.then set-routes)) + (-> (js/Promise.all optimized-routes) + (.then set-optimized-routes))) + (do (set-routes nil) + (set-optimized-routes nil))) + #()) + 500) + #()) + (array selected-date)) + + (use-map-items + loading + {:tasks (when routes (mapv (fn [route] {:route route}) routes)) + :places current-date-created-places} + [selected-date routes]) + + [map-layout {:title (if loading (str (tr [:misc/loading]) "...") name)} + [:div {:class "p-4 w-full h-full overflow-auto"} + [combobox + {:class "w-full" + :options grouped-locations + :value selected-date + :option-to-label (fn [[date _]] date) + :option-to-value (fn [[date _]] date) + :on-change #(do + (set-selected-date %) + (set-current-location-index 0))}] + + (when (seq routes) + [:<> + [:div {:class "flex items-center"} + [:div {:class "text-sm text-gray-600"} + (str routes-total-travel-distance " m")] + [:div {:class "text-sm text-gray-600"} + (str routes-total-travel-duration " s")]] + [:div {:class "flex items-center"} + [:div {:class "text-sm text-gray-600"} + (str optimized-total-travel-distance " m")] + [:div {:class "text-sm text-gray-600"} + (str optimized-total-travel-duration " s")]]])]])) diff --git a/dispatch/src/ui/views/organization/agent/performance.cljs b/dispatch/src/ui/views/organization/agent/performance.cljs new file mode 100644 index 00000000..0806452e --- /dev/null +++ b/dispatch/src/ui/views/organization/agent/performance.cljs @@ -0,0 +1,145 @@ +(ns ui.views.organization.agent.performance + (:require ["react" :refer (useState useEffect)] + [re-frame.core :refer (dispatch)] + [shadow.resource :refer (inline)] + [common.utils.date :refer (parse-date)] + [common.utils.promise :refer (each)] + [ui.lib.apollo :refer (gql use-query)] + [ui.lib.google.maps.directions :refer (calc-route calc-optimized-route)] + [ui.utils.input :refer (debounce-cb)] + [ui.utils.color :refer (get-color)] + [ui.utils.date :as d] + [ui.utils.i18n :refer (tr)] + [ui.hooks.use-map :refer (use-map-items)] + [ui.components.layout.map :refer (map-layout)] + [ui.components.inputs.date :refer (date-select)] + [ui.views.organization.agent.performance :as performance])) + +(def FETCH_ORGANIZATION_AGENT_PERFORMANCE (gql (inline "queries/user/organization/fetch-agent-performance.graphql"))) + +(defn connected-partition [n coll] + (when (seq coll) + (lazy-seq (cons (take n coll) (connected-partition n (drop (dec n) coll)))))) + +(defn partitioned-calcs [calc-fn places] + (let [partitions (connected-partition 25 places) + route-fns (map (fn [partition] #(calc-fn partition)) partitions)] + (-> (each route-fns) + (.then (fn [routes] + (reduce (fn [acc route] + {:legs (concat (:legs acc) (:legs route)) + :path (concat (:path acc) (:path route))}) + {} + routes)))))) + +(defn view [] + (let [[selected-date set-selected-date] (useState (js/Date. "2023-02-28")) + query (use-query FETCH_ORGANIZATION_AGENT_PERFORMANCE {:variables {:start (d/startOfDay selected-date) + :end (d/endOfDay selected-date)}}) + {:keys [data loading]} query + {:keys [performance]} (some-> data :user :organization) + [routes set-routes] (useState []) + [optimized-routes set-optimized-routes] (useState nil) + agents (->> performance + (filterv #(> (count (:places %)) 2)))] + + (useEffect + (fn [] + (if (and (not loading) + (seq agents)) + (let [places-groups (->> agents (mapv :places)) + routes-fns (mapv + (fn [places] + (fn [] + (if (seq places) + (partitioned-calcs calc-route places) + (js/Promise.resolve)))) + places-groups) + optimized-routes-fns (mapv + (fn [places] + (fn [] + (if (seq places) + (partitioned-calcs calc-optimized-route places) + (js/Promise.resolve)))) + places-groups)] + (-> (each routes-fns) + (.then set-routes)) + (-> (each optimized-routes-fns) + (.then set-optimized-routes))) + (do (set-routes nil) + (set-optimized-routes nil))) + #()) + (array loading)) + + (use-map-items + loading + {:tasks (when routes (mapv (fn [route] (when route {:route route})) (concat routes optimized-routes))) + :places (->> agents + (map-indexed (fn [idx {:keys [places]}] + (map (fn [{:keys [_ lat lng]}] + {:name " " + :color (get-color idx) + :lat lat + :lng lng}) + places))) + flatten)} + [performance routes]) + + [map-layout {:title (if loading (str (tr [:misc/loading]) "...") (or (some-> selected-date (d/format "yyyy-MM-dd")) (tr [:field/date])))} + [:div {:class "p-4 w-full h-full overflow-auto"} + [date-select + {:label (tr [:field/date]) + :placeholder (tr [:field/date]) + :value selected-date + :required true + :class "mb-4" + :on-select set-selected-date}] + (doall + (for [[idx {:keys [id name]}] (->> agents (map-indexed vector))] + (let [route (get routes idx) + optimized-route (get optimized-routes idx) + route-total-travel-distance (->> route :legs (map :distance) (reduce +)) + route-total-travel-time (->> route :legs (map :duration) (reduce +)) + route-total-km (-> route-total-travel-distance (/ 1000) (.toFixed 2)) + route-total-min (-> route-total-travel-time (/ 60) (.toFixed 2)) + optimized-route-total-travel-distance (->> optimized-route :legs (map :distance) (reduce +)) + optimized-route-total-travel-time (->> optimized-route :legs (map :duration) (reduce +)) + optimized-route-total-km (-> optimized-route-total-travel-distance (/ 1000) (.toFixed 2)) + optimized-route-total-min (-> optimized-route-total-travel-time (/ 60) (.toFixed 2)) + difference-km (-> (- route-total-travel-distance optimized-route-total-travel-distance) (/ 1000) (.toFixed 2)) + difference-min (-> (- route-total-travel-time optimized-route-total-travel-time) (/ 60) (.toFixed 2))] + ^{:key id} + [:div {:class "mb-4"} + [:h3 {:class "text-sm font-bold mb-2"} name] + (when route + [:div {:class "grid grid-cols-2 gap-4"} + [:div {:class "mb-2"} + [:div {:class "text-neutral-400 text-xs"} + (tr [:field/total-travel-distance "Actual travel distance"])] + [:div {:class "text-sm"} + route-total-km " km"]] + [:div {:class "mb-2"} + [:div {:class "text-neutral-400 text-xs"} + (tr [:field/total-travel-time "Actual travel time"])] + [:div {:class "text-sm"} + route-total-min " min"]] + [:div {:class "mb-2"} + [:div {:class "text-neutral-400 text-xs"} + (tr [:field/total-travel-distance-optimized "Optimized travel distance"])] + [:div {:class "text-sm"} + optimized-route-total-km " km"]] + [:div {:class "mb-2"} + [:div {:class "text-neutral-400 text-xs"} + (tr [:field/total-travel-time-optimized "Optimized travel time"])] + [:div {:class "text-sm"} + optimized-route-total-min " min"]] + [:div {:class "mb-2"} + [:div {:class "text-neutral-400 text-xs"} + (tr [:field/total-travel-distance-difference "Travel distance difference"])] + [:div {:class "text-sm"} + difference-km " km"]] + [:div {:class "mb-2"} + [:div {:class "text-neutral-400 text-xs"} + (tr [:field/total-travel-time-difference "Travel time difference"])] + [:div {:class "text-sm"} + difference-min " min"]]])])))]])) diff --git a/dispatch/src/ui/views/organization/core.cljs b/dispatch/src/ui/views/organization/core.cljs index 3e1e53fa..a0aea64a 100644 --- a/dispatch/src/ui/views/organization/core.cljs +++ b/dispatch/src/ui/views/organization/core.cljs @@ -22,11 +22,13 @@ {:path "tasks/:task" :element [task/detail-view]} {:path "tasks/:task/update" :element [task/update-view]} {:path "agents" :element [agent/list-view]} - {:path "agents/:agent" :element [agent/detail-view]} {:path "agents/create" :element [agent/create-view]} + {:path "agents/performance" :element [agent/performance-view]} + {:path "agents/:agent" :element [agent/detail-view]} + {:path "agents/:agent/locations" :element [agent/locations-view]} {:path "places" :element [place/list-view]} - {:path "places/:place" :element [place/detail-view]} {:path "places/create" :element [place/create-view]} + {:path "places/:place" :element [place/detail-view]} {:path "stops/:stop" :element [stop/detail-view]} {:path "vehicles" :element [vehicle/list-view]} {:path "shipments" :element [shipment/list-view]}