diff --git a/backend/bloom/domain/metrics.py b/backend/bloom/domain/metrics.py index d59af644..c7294177 100644 --- a/backend/bloom/domain/metrics.py +++ b/backend/bloom/domain/metrics.py @@ -39,6 +39,11 @@ class ResponseMetricsVesselInActivitySchema(BaseModel): vessel: VesselListView total_time_at_sea: Optional[timedelta] +class ResponseMetricsVesselInZonesSchema(BaseModel): + model_config = ConfigDict(from_attributes=True) + vessel: VesselListView + total_time_in_zones: Optional[timedelta] + class ResponseMetricsZoneVisitedSchema(BaseModel): zone: ZoneListView visiting_duration: timedelta diff --git a/backend/bloom/routers/v1/metrics.py b/backend/bloom/routers/v1/metrics.py index 0f3e6cdf..4cd3afbf 100644 --- a/backend/bloom/routers/v1/metrics.py +++ b/backend/bloom/routers/v1/metrics.py @@ -123,3 +123,44 @@ async def read_metrics_all_vessels_visiting_time_by_zone(request: Request, category=category, sub_category=sub_category) return jsonable_encoder(payload) + + +@router.get("/metrics/vessels-activity") +# @cache +async def read_metrics_all_vessels_visiting_time_in_zones( + request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + category: Optional[str] = None, + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), +): + check_apikey(key) + use_cases = UseCases() + MetricsService = use_cases.metrics_service() + payload = MetricsService.get_vessels_activity_in_zones( + datetime_range=datetime_range, + pagination=pagination, + order=order, + category=category, + ) + return jsonable_encoder(payload) + + +@router.get("/metrics/zones-visited") +# @cache +async def read_metrics_all_zones_visited( + request: Request, + datetime_range: DatetimeRangeRequest = Depends(), + category: Optional[str] = None, + pagination: PageParams = Depends(), + order: OrderByRequest = Depends(), + key: str = Depends(X_API_KEY_HEADER), +): + check_apikey(key) + use_cases = UseCases() + MetricsService = use_cases.metrics_service() + payload = MetricsService.get_zones_visited( + datetime_range=datetime_range, pagination=pagination, order=order, category=category + ) + return jsonable_encoder(payload) diff --git a/backend/bloom/services/metrics.py b/backend/bloom/services/metrics.py index c9a08ef1..1059e214 100644 --- a/backend/bloom/services/metrics.py +++ b/backend/bloom/services/metrics.py @@ -16,11 +16,22 @@ from bloom.infra.repositories.repository_zone import ZoneRepository from bloom.domain.metrics import TotalTimeActivityTypeRequest -from bloom.domain.metrics import (ResponseMetricsVesselInActivitySchema, - ResponseMetricsZoneVisitedSchema, - ResponseMetricsZoneVisitingTimeByVesselSchema, - ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, - ResponseMetricsVesselVisitingTimeByZoneSchema) +from bloom.domain.metrics import ( + ResponseMetricsVesselInActivitySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsVesselInZonesSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema, + ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, + ResponseMetricsVesselVisitingTimeByZoneSchema, +) +from bloom.domain.metrics import ( + ResponseMetricsVesselInActivitySchema, + ResponseMetricsZoneVisitedSchema, + ResponseMetricsVesselInZonesSchema, + ResponseMetricsZoneVisitingTimeByVesselSchema, + ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema, + ResponseMetricsVesselVisitingTimeByZoneSchema, +) class MetricsService(): def __init__( @@ -72,7 +83,108 @@ def getVesselsInActivity(self, total_time_at_sea=item[1] )\ for item in payload] - + + def get_vessels_activity_in_zones( + self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest, + category: Optional[str] = None, + ): + payload=[] + with self.session_factory() as session: + stmt = ( + select( + sql_model.Vessel, + func.sum(sql_model.Metrics.duration_total).label( + "total_time_in_zones" + ), + ) + .select_from(sql_model.Metrics) + .join( + sql_model.Vessel, + sql_model.Metrics.vessel_id == sql_model.Vessel.id + ) + .join( + sql_model.Zone, + sql_model.Metrics.zone_id == sql_model.Zone.id + ) + .where( + sql_model.Metrics.timestamp.between( + datetime_range.start_at, datetime_range.end_at + ) + ) + .group_by( + sql_model.Vessel + ) + ) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + if category: + stmt = stmt.where(sql_model.Zone.category == category) + stmt = ( + stmt.order_by(asc("total_time_in_zones")) + if order.order == OrderByEnum.ascending + else stmt.order_by(desc("total_time_in_zones")) + ) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload=session.execute(stmt).all() + + return [ + ResponseMetricsVesselInZonesSchema( + vessel=VesselRepository.map_to_domain(item[0]).model_dump(), + total_time_in_zones=item[1], + ) + for item in payload + ] + + def get_zones_visited( + self, + datetime_range: DatetimeRangeRequest, + pagination: PageParams, + order: OrderByRequest, + category: Optional[str] = None, + ): + payload = [] + with self.session_factory() as session: + stmt = ( + select( + sql_model.Zone, + func.sum(sql_model.Metrics.duration_total).label( + "visiting_duration" + ), + ) + .select_from(sql_model.Metrics) + .join( + sql_model.Zone, + sql_model.Zone.id == sql_model.Metrics.zone_id, + ) + .where( + sql_model.Metrics.timestamp.between( + datetime_range.start_at, datetime_range.end_at + ) + ) + .where(sql_model.Metrics.zone_category == category) + .group_by(sql_model.Zone) + ) + stmt = stmt.offset(pagination.offset) if pagination.offset != None else stmt + if category: + stmt = stmt.where(sql_model.Zone.category == category) + stmt = ( + stmt.order_by(asc("visiting_duration")) + if order.order == OrderByEnum.ascending + else stmt.order_by(desc("visiting_duration")) + ) + stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt + payload = session.execute(stmt).all() + + return [ + ResponseMetricsZoneVisitedSchema( + zone=ZoneRepository.map_to_domain(item[0]).model_dump(), + visiting_duration=item[1], + ) + for item in payload + ] + def getVesselsAtSea(self, datetime_range: DatetimeRangeRequest, ): @@ -95,7 +207,7 @@ def getVesselsAtSea(self, ) return session.execute(stmt).scalar() - + def getZoneVisited(self, datetime_range: DatetimeRangeRequest, pagination: PageParams, @@ -137,7 +249,8 @@ def getZoneVisited(self, visiting_duration=item[1] )\ for item in payload] - + + def getZoneVisitingTimeByVessel(self, zone_id: int, datetime_range: DatetimeRangeRequest, @@ -145,7 +258,7 @@ def getZoneVisitingTimeByVessel(self, pagination: PageParams,): payload=[] with self.session_factory() as session: - + stmt=select( sql_model.Zone, sql_model.Vessel, @@ -166,7 +279,8 @@ def getZoneVisitingTimeByVessel(self, ) )\ .group_by(sql_model.Zone.id,sql_model.Vessel.id) - + + stmt = stmt.order_by(func.sum(sql_model.Segment.segment_duration).asc())\ if order.order == OrderByEnum.ascending \ else stmt.order_by(func.sum(sql_model.Segment.segment_duration).desc()) @@ -185,7 +299,8 @@ def getZoneVisitingTimeByVessel(self, zone_visiting_time_by_vessel=item[2] )\ for item in payload] - + + def getVesselVisitingTimeByZone(self, order: OrderByRequest, datetime_range: DatetimeRangeRequest, @@ -226,14 +341,13 @@ def getVesselVisitingTimeByZone(self, stmt = stmt.limit(pagination.limit) if pagination.limit != None else stmt if vessel_id is not None: stmt=stmt.where(sql_model.Vessel.id==vessel_id) - - + + return [ResponseMetricsVesselVisitingTimeByZoneSchema( vessel=VesselListView(**VesselRepository.map_to_domain(model[0]).model_dump()), zone=ZoneListView(**ZoneRepository.map_to_domain(model[1]).model_dump()), vessel_visiting_time_by_zone=model[2]) for model in session.execute(stmt).all()] - def getVesselVisitsByActivityType(self, vessel_id: int, activity_type: TotalTimeActivityTypeRequest, @@ -261,10 +375,11 @@ def getVesselVisitsByActivityType(self, literal_column('0 seconds'), )) payload=session.execute(stmt.limit(1)).scalar_one_or_none() - + + return [ ResponseMetricsVesselTotalTimeActivityByActivityTypeSchema( vessel_id=item.id, activity=item.activity, total_activity_time=item.total_activity_time, ) for item in payload] - \ No newline at end of file + diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 95bd06e8..46457ba5 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,11 +1,17 @@ -"use client" +"use client"; + +import { useMemo, useState } from "react"; +import { useDashboardData } from "@/services/dashboard.service"; + + + +import { getDateRange } from "@/libs/dateUtils"; +import DashboardHeader from "@/components/dashboard/dashboard-header"; +import DashboardOverview from "@/components/dashboard/dashboard-overview"; + + -import { useMemo, useState } from "react" -import { useDashboardData } from "@/services/dashboard.service" -import { getDateRange } from "@/libs/dateUtils" -import DashboardHeader from "@/components/dashboard/dashboard-header" -import DashboardOverview from "@/components/dashboard/dashboard-overview" export default function DashboardPage() { const [selectedDays, setSelectedDays] = useState(7) @@ -14,7 +20,7 @@ export default function DashboardPage() { }, [selectedDays]) const { - topVesselsInActivity, + topVesselsInMpas, topAmpsVisited, totalVesselsInActivity, totalAmpsVisited, @@ -31,7 +37,7 @@ export default function DashboardPage() {
{ setSelectedDays(Number(value)) }} - topVesselsInActivityLoading={isLoading.topVesselsInActivity} + topVesselsInMpasLoading={isLoading.topVesselsInMpas} topAmpsVisitedLoading={isLoading.topAmpsVisited} totalVesselsActiveLoading={isLoading.totalVesselsInActivity} totalAmpsVisitedLoading={isLoading.totalAmpsVisited} @@ -49,4 +55,4 @@ export default function DashboardPage() {
) -} +} \ No newline at end of file diff --git a/frontend/components/dashboard/dashboard-overview.tsx b/frontend/components/dashboard/dashboard-overview.tsx index 7f4c3c1d..ad992733 100644 --- a/frontend/components/dashboard/dashboard-overview.tsx +++ b/frontend/components/dashboard/dashboard-overview.tsx @@ -1,16 +1,21 @@ -"use client" +"use client"; -import { TOTAL_AMPS, TOTAL_VESSELS } from "@/constants/totals.constants" +import { TOTAL_AMPS, TOTAL_VESSELS } from "@/constants/totals.constants"; -import { Item } from "@/types/item" -import ListCard from "@/components/ui/list-card" -import KPICard from "@/components/dashboard/kpi-card" -import { DateRangeSelector } from "../ui/date-range-selector" + +import { Item } from "@/types/item"; +import ListCard from "@/components/ui/list-card"; +import KPICard from "@/components/dashboard/kpi-card"; + + + +import { DateRangeSelector } from "../ui/date-range-selector"; + type Props = { - topVesselsInActivity: Item[] - topVesselsInActivityLoading: boolean + topVesselsInMpas: Item[] + topVesselsInMpasLoading: boolean topAmpsVisited: Item[] topAmpsVisitedLoading: boolean totalVesselsActive: number @@ -23,8 +28,8 @@ type Props = { } export default function DashboardOverview({ - topVesselsInActivity, - topVesselsInActivityLoading, + topVesselsInMpas, + topVesselsInMpasLoading, topAmpsVisited, topAmpsVisitedLoading, totalVesselsActive, @@ -74,9 +79,9 @@ export default function DashboardOverview({ @@ -84,4 +89,4 @@ export default function DashboardOverview({ ) -} +} \ No newline at end of file diff --git a/frontend/libs/mapper.tsx b/frontend/libs/mapper.tsx index 9799cb0e..e0ef0a9e 100644 --- a/frontend/libs/mapper.tsx +++ b/frontend/libs/mapper.tsx @@ -11,7 +11,7 @@ export function convertVesselDtoToItem(metrics: VesselMetrics[]): Item[] { id: `${vessel.id}`, title: vessel.ship_name, description: `IMO ${vessel.imo} / MMSI ${vessel.mmsi} / ${vessel.length} m`, - value: convertDurationToString(vesselMetrics.total_time_at_sea), + value: convertDurationToString(vesselMetrics.total_time_in_zones), type: "vessel", countryIso3: vessel.country_iso3, } diff --git a/frontend/services/backend-rest-client.ts b/frontend/services/backend-rest-client.ts index 58293271..fbd05297 100644 --- a/frontend/services/backend-rest-client.ts +++ b/frontend/services/backend-rest-client.ts @@ -114,12 +114,12 @@ export async function getVesselFirstExcursionSegments(vesselId: number) { } } -export function getTopVesselsInActivity( +export function getTopVesselsInMpas( startAt: string, endAt: string, topVesselsLimit: number ) { - const url = `${BASE_URL}/metrics/vessels-in-activity?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` + const url = `${BASE_URL}/metrics/vessels-activity?category=amp&start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC` console.log(`GET ${url}`) return axios.get(url) } @@ -130,7 +130,7 @@ export function getTopZonesVisited( topZonesLimit: number, category?: string ) { - const url = `${BASE_URL}/metrics/zone-visited?${ + const url = `${BASE_URL}/metrics/zones-visited?${ category ? `category=${category}&` : "" }start_at=${startAt}&end_at=${endAt}&limit=${topZonesLimit}&order=DESC` console.log(`GET ${url}`) diff --git a/frontend/services/dashboard.service.ts b/frontend/services/dashboard.service.ts index ef74e743..832c5b96 100644 --- a/frontend/services/dashboard.service.ts +++ b/frontend/services/dashboard.service.ts @@ -1,6 +1,6 @@ import { TOTAL_VESSELS } from "@/constants/totals.constants" import { - getTopVesselsInActivity, + getTopVesselsInMpas, getTopZonesVisited, getVesselsAtSea, getVesselsTrackedCount, @@ -13,13 +13,13 @@ import { convertVesselDtoToItem, convertZoneDtoToItem } from "@/libs/mapper" const TOP_ITEMS_SIZE = 5 type DashboardData = { - topVesselsInActivity: any[] + topVesselsInMpas: any[] topAmpsVisited: any[] totalVesselsInActivity: number totalAmpsVisited: number totalVesselsTracked: number isLoading: { - topVesselsInActivity: boolean + topVesselsInMpas: boolean topAmpsVisited: boolean totalVesselsInActivity: boolean totalAmpsVisited: boolean @@ -32,13 +32,13 @@ export const useDashboardData = ( endAt: string ): DashboardData => { const { - data: topVesselsInActivity = [], - isLoading: topVesselsInActivityLoading, + data: topVesselsInMpas = [], + isLoading: topVesselsInMpasLoading, } = useSWR( - `topVesselsInActivity-${startAt}`, + `topVesselsInMpas-${startAt}`, async () => { try { - const response = await getTopVesselsInActivity( + const response = await getTopVesselsInMpas( startAt, endAt, TOP_ITEMS_SIZE @@ -46,7 +46,7 @@ export const useDashboardData = ( return convertVesselDtoToItem(response?.data || []) } catch (error) { console.log( - "An error occurred while fetching top vessels in activity: " + error + "An error occurred while fetching top vessels in MPAs: " + error ) return [] } @@ -137,13 +137,13 @@ export const useDashboardData = ( ) return { - topVesselsInActivity, + topVesselsInMpas, topAmpsVisited, totalVesselsInActivity, totalAmpsVisited, totalVesselsTracked, isLoading: { - topVesselsInActivity: topVesselsInActivityLoading, + topVesselsInMpas: topVesselsInMpasLoading, topAmpsVisited: topAmpsVisitedLoading, totalVesselsInActivity: totalVesselsInActivityLoading, totalAmpsVisited: totalAmpsVisitedLoading, diff --git a/frontend/types/vessel.ts b/frontend/types/vessel.ts index ca4ff5e7..43153845 100644 --- a/frontend/types/vessel.ts +++ b/frontend/types/vessel.ts @@ -19,7 +19,7 @@ export type Vessel = { export type VesselMetrics = { vessel: VesselDetails - total_time_at_sea: string + total_time_in_zones: string } export type VesselDetails = {