Skip to content

Commit

Permalink
Merge pull request #211 from dataforgoodfr/ctiss-dashboard-call-backend
Browse files Browse the repository at this point in the history
feat: Display backend data on Dashboard page
  • Loading branch information
ComeTiss authored Oct 16, 2024
2 parents 842b64c + 6a6b122 commit 7d2aef5
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 38 deletions.
2 changes: 1 addition & 1 deletion frontend/.env.local
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
NEXT_PUBLIC_MAPTILER_TO=3IWed9ZbNv0p8UVD6Ogv
NEXT_PUBLIC_DOMAIN=http://localhost:3000

NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8000
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8000/api/v1
NEXT_PUBLIC_BACKEND_API_KEY=bloom
73 changes: 71 additions & 2 deletions frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,84 @@
import DashboardHeader from "@/components/dashboard/dashboard-header"
import DashboardOverview from "@/components/dashboard/dashboard-overview"

export default function DashboardPage() {
import { getTopVesselsInActivity, getTopZonesVisited } from "@/services/backend-rest-client"
import { format } from "@/libs/dateUtils";

const DAYS_SINCE_TODAY = 360
const TOP_ITEMS_SIZE = 5

async function fetchTopVesselsInActivity() {
try {
let today = new Date();
let startAt = new Date(new Date().setDate(today.getDate() - DAYS_SINCE_TODAY));
const response = await getTopVesselsInActivity(format(startAt), format(today), TOP_ITEMS_SIZE);
return response?.data;

} catch(error) {
console.log("An error occured while fetching top vessels in activity : " + error)
return [];
}
}

async function fetchTopAmpsVisited() {
try {
let today = new Date();
let startAt = new Date(new Date().setDate(today.getDate() - DAYS_SINCE_TODAY));
const response = await getTopZonesVisited(format(startAt), format(today), TOP_ITEMS_SIZE);
return response?.data;

} catch(error) {
console.log("An error occured while fetching top amps visited: " + error)
return [];
}
}

async function fetchTotalVesselsInActivity() {
try {
let today = new Date();
let startAt = new Date(new Date().setDate(today.getDate() - DAYS_SINCE_TODAY));
// TODO(CT): replace with new endpoint (waiting for Hervé)
const response = await getTopVesselsInActivity(format(startAt), format(today), 10000); // high value to capture all data
return response?.data?.length;

} catch(error) {
console.log("An error occured while fetching top amps visited: " + error)
return 0;
}
}

async function fetchTotalAmpsVisited() {
try {
let today = new Date();
let startAt = new Date(new Date().setDate(today.getDate() - DAYS_SINCE_TODAY));
// TODO(CT): replace with new endpoint (waiting for Hervé)
const response = await getTopZonesVisited(format(startAt), format(today), 10000); // high value to capture all data
return response?.data?.length;

} catch(error) {
console.log("An error occured while fetching top amps visited: " + error)
return 0;
}
}

export default async function DashboardPage() {
const topVesselsInActivity = await fetchTopVesselsInActivity();
const topAmpsVisited = await fetchTopAmpsVisited();
const totalVesselsInActivity = await fetchTotalVesselsInActivity();
const totalAmpsVisited = await fetchTotalAmpsVisited();

return (
<section className="h-svh bg-color-3 px-6">
<div className="block h-1/6 w-full">
<DashboardHeader />
</div>

<div className="h-5/6 w-full">
<DashboardOverview />
<DashboardOverview
topVesselsInActivity={topVesselsInActivity}
topAmpsVisited={topAmpsVisited}
totalVesselsActive={totalVesselsInActivity}
totalAmpsVisited={totalAmpsVisited} />
</div>
</section>
)
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/core/map/main-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export default function CoreMap({ vesselsPositions }: CoreMapProps) {
)
}
function toSegmentsGeo({ segments, vesselId }: VesselExcursionSegments): any {
const segmentsGeo = segments.map((segment: VesselExcursionSegment) => {
const segmentsGeo = segments?.map((segment: VesselExcursionSegment) => {
return {
speed: segment.average_speed,
navigational_status: "unknown",
Expand All @@ -205,6 +205,6 @@ function toSegmentsGeo({ segments, vesselId }: VesselExcursionSegments): any {
}
}
})
return { vesselId, type: "FeatureCollection", features: segmentsGeo };
return { vesselId, type: "FeatureCollection", features: segmentsGeo ?? [] };
}

46 changes: 22 additions & 24 deletions frontend/components/dashboard/dashboard-overview.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
"use client"

import mockData from "@/public/data/mock-data-dashboard.json"

import Dropdown from "@/components/ui/dropdown"
import ListCard from "@/components/ui/list-card"
import KPICard from "@/components/dashboard/kpi-card"
import { ZoneVisitTimeDto } from "@/types/zone"
import { VesselTrackingTimeDto } from "@/types/vessel"
import { convertVesselDtoToItem, convertZoneDtoToItem } from "@/libs/mapper";

const TOTAL_VESSELS = 1700;
const TOTAL_AMPS = 720;

type Props = {
topVesselsInActivity: VesselTrackingTimeDto[];
topAmpsVisited: ZoneVisitTimeDto[];
totalVesselsActive: number;
totalAmpsVisited: number;
}

// TODO: use real data + load in server components
let amps = mockData["top-amps"]
let vessels = mockData["top-vessels"]
let totalVesselsActive = mockData["total-vessels-active"]
let totalVessels = mockData["total-vessels"]
let totalAmpsVisited = mockData["total-amps-visited"]
let totalAmps = mockData["total-amps"]
let fishingEffort = mockData["fishing-effort-hours"]
let fishingArea = mockData["fishing-area-km2"]
export default function DashboardOverview(props : Props) {
const { topVesselsInActivity, topAmpsVisited, totalVesselsActive, totalAmpsVisited } = props;
const topVesselsInActivityToItems = convertVesselDtoToItem(topVesselsInActivity);
const topAmpsVisitedToItems = convertZoneDtoToItem(topAmpsVisited);

export default function DashboardOverview() {
return (
<section className="grid">
<div className="mb-2 w-full">
<Dropdown
className="float-right w-40"
options={["7 derniers jours", "30 derniers jours"]} // TODO: move in dedicated enum?
options={["360 derniers jours"]} // TODO: move in dedicated enum?
onSelect={(value) => console.log("selected: " + value)}
/>
</div>
Expand All @@ -33,19 +38,12 @@ export default function DashboardOverview() {
<KPICard
title="Total vessels in Activity"
kpiValue={totalVesselsActive}
totalValue={totalVessels}
totalValue={TOTAL_VESSELS}
/>
<KPICard
title="Total AMPs visited"
kpiValue={totalAmpsVisited}
totalValue={totalAmps}
/>
<KPICard
title="Fishing effort"
kpiValue={fishingEffort}
kpiUnit="Hours"
totalValue={fishingArea}
totalUnit="Km2"
totalValue={TOTAL_AMPS}
/>
</div>
</div>
Expand All @@ -54,12 +52,12 @@ export default function DashboardOverview() {
<div className="grid grid-cols-1 gap-y-4">
<ListCard
title="Top AMPs visited during the period"
items={amps}
items={topAmpsVisitedToItems ?? []}
enableViewDetails
/>
<ListCard
title="Top Vessels visiting AMPS"
items={vessels}
items={topVesselsInActivityToItems ?? []}
enableViewDetails
/>
</div>
Expand Down
8 changes: 7 additions & 1 deletion frontend/components/ui/list-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ type Props = {
}

export default function ListCard({ title, items, enableViewDetails }: Props) {
const hasNoData = !!(items?.length == 0)

return (
<div className="min-h-50 rounded bg-color-2 pl-5 pr-10 pt-2">
<div className="mb-3 block text-xs font-semibold uppercase text-white">
{title}
</div>

{hasNoData && (
<div className="mb-3 block text-xxs font-light text-white">
No data found
</div>
)}
{items.map((item) => (
<ListItem
item={item}
Expand Down
29 changes: 29 additions & 0 deletions frontend/libs/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

import { Temporal } from '@js-temporal/polyfill';

export function convertDurationInHours(durationPattern: string): number {
const duration = Temporal.Duration.from(durationPattern);
const durationHours = duration.total({ relativeTo: Temporal.Now.plainDateISO(), unit: "hours"})
return ~~durationHours; // round without decimals
}

// format date like: 2023-07-15T17:16:27
export function format(date: Date) {
return (
[
date.getFullYear(),
padTwoDigits(date.getMonth() + 1),
padTwoDigits(date.getDate()),
].join("-") +
"T" +
[
padTwoDigits(date.getHours()),
padTwoDigits(date.getMinutes()),
padTwoDigits(date.getSeconds()),
].join(":")
);
}

function padTwoDigits(num: number) {
return num.toString().padStart(2, "0");
}
29 changes: 29 additions & 0 deletions frontend/libs/mapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Item } from "@/types/item";
import { VesselTrackingTimeDto } from "@/types/vessel";
import { ZoneVisitTimeDto } from "@/types/zone";

import { convertDurationInHours } from "@/libs/dateUtils";

export function convertVesselDtoToItem(vesselDtos: VesselTrackingTimeDto[]): Item[] {
return vesselDtos?.map((vesselDto: VesselTrackingTimeDto) => {
return {
id: `${vesselDto.vessel_id}`,
title: vesselDto.vessel_ship_name,
description: `IMO ${vesselDto.vessel_imo} / MMSI ${vesselDto.vessel_mmsi} / ${vesselDto.vessel_length} mètres`,
value: `${convertDurationInHours(vesselDto.total_time_at_sea)}h`,
type: "vessel"
}
});
}

export function convertZoneDtoToItem(zoneDtos: ZoneVisitTimeDto[]): Item[] {
return zoneDtos?.map((zoneDto: ZoneVisitTimeDto) => {
return {
id: `${zoneDto.zone_id}`,
title: zoneDto.zone_name,
description: zoneDto.zone_sub_category,
value: `${convertDurationInHours(zoneDto.visiting_duration)}h`,
type: "amp"
}
})
}
44 changes: 36 additions & 8 deletions frontend/services/backend-rest-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

import { Vessel, VesselExcursion, VesselExcursionSegment, VesselPositions } from "@/types/vessel";
import { Vessel, VesselExcursion, VesselExcursionSegment, VesselPositions, VesselTrackingTimeDto } from "@/types/vessel";
import { ZoneVisitTimeDto } from "@/types/zone";
import axios, { InternalAxiosRequestConfig } from "axios";
import { log } from "console";

const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_BASE_URL;
const API_KEY = process.env.NEXT_PUBLIC_BACKEND_API_KEY ?? 'no-key-found';
Expand All @@ -12,23 +14,49 @@ axios.interceptors.request.use((request: InternalAxiosRequestConfig) => {
});

export function getVessels() {
return axios.get<Vessel[]>(`${BASE_URL}/vessels`);
const url = `${BASE_URL}/vessels`;
console.log(`GET ${url}`);
return axios.get<Vessel[]>(url);
}

export function getVesselsLatestPositions() {
return axios.get<VesselPositions>(`${BASE_URL}/vessels/all/positions/last`);
const url = `${BASE_URL}/vessels/all/positions/last`;
console.log(`GET ${url}`);
return axios.get<VesselPositions>(url);
}

export function getVesselExcursion(vesselId: number) {
return axios.get<VesselExcursion[]>(`${BASE_URL}/vessels/${vesselId}/excursions`);
const url = `${BASE_URL}/vessels/${vesselId}/excursions`;
console.log(`GET ${url}`);
return axios.get<VesselExcursion[]>(url);
}

export function getVesselSegments(vesselId: number, excursionId: number) {
return axios.get<VesselExcursionSegment[]>(`${BASE_URL}/vessels/${vesselId}/excursions/${excursionId}/segments`);
const url = `${BASE_URL}/vessels/${vesselId}/excursions/${excursionId}/segments`;
console.log(`GET ${url}`);
return axios.get<VesselExcursionSegment[]>(url);
}

export async function getVesselFirstExcursionSegments(vesselId: number) {
const response = await getVesselExcursion(vesselId);
const excursionId = response?.data[0]?.id;
return !!excursionId ? getVesselSegments(vesselId, excursionId) : [];
try {
const response = await getVesselExcursion(vesselId);
const excursionId = response?.data[0]?.id;
return !!excursionId ? getVesselSegments(vesselId, excursionId) : [];

} catch(error) {
console.error(error);
return [];
}
}

export function getTopVesselsInActivity(startAt: string, endAt: string, topVesselsLimit: number) {
const url = `${BASE_URL}/metrics/vessels-in-activity?start_at=${startAt}&end_at=${endAt}&limit=${topVesselsLimit}&order=DESC`;
console.log(`GET ${url}`);
return axios.get<VesselTrackingTimeDto[]>(url);
}

export function getTopZonesVisited(startAt: string, endAt: string, topZonesLimit: number) {
const url = `${BASE_URL}/metrics/zone-visited?start_at=${startAt}&end_at=${endAt}&limit=${topZonesLimit}&order=DESC`;
console.log(`GET ${url}`);
return axios.get<ZoneVisitTimeDto[]>(url);
}
21 changes: 21 additions & 0 deletions frontend/types/vessel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ export type Vessel = {
length_class: string
}

export type VesselTrackingTimeDto = {
vessel_id: number;
vessel_mmsi: number;
vessel_ship_name: string;
vessel_width: number;
vessel_length: number;
vessel_country_iso3: string;
vessel_type: string;
vessel_imo: number;
vessel_cfr: string;
vessel_external_marking: string;
vessel_ircs: string;
vessel_home_port_id: number;
vessel_details: string;
vessel_tracking_activated: boolean
vessel_tracking_status: string;
vessel_length_class: string;
vessel_check: string;
total_time_at_sea: string;
}

export type VesselPositions = VesselPosition[]

export interface VesselPosition {
Expand Down
8 changes: 8 additions & 0 deletions frontend/types/zone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export type ZoneVisitTimeDto = {
zone_id: number;
zone_category: string;
zone_sub_category: string;
zone_name: string;
visiting_duration: string;
}

0 comments on commit 7d2aef5

Please sign in to comment.