From 4c618c57026e073a0b8cd31c077b79c8fe6e7603 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 14 Feb 2024 15:01:30 +0100 Subject: [PATCH 01/82] Create pno types tables --- .../internal/V0.244__Create_pno_types.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 backend/src/main/resources/db/migration/internal/V0.244__Create_pno_types.sql diff --git a/backend/src/main/resources/db/migration/internal/V0.244__Create_pno_types.sql b/backend/src/main/resources/db/migration/internal/V0.244__Create_pno_types.sql new file mode 100644 index 0000000000..813e9a8580 --- /dev/null +++ b/backend/src/main/resources/db/migration/internal/V0.244__Create_pno_types.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS pno_types ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + minimum_notification_period DOUBLE PRECISION NOT NULL DEFAULT -1, + has_designated_ports BOOLEAN NOT NULL DEFAULT False +); + +CREATE TABLE IF NOT EXISTS pno_type_rules ( + id SERIAL PRIMARY KEY, + pno_type_id INTEGER NOT NULL REFERENCES pno_types (id), + species VARCHAR[] NOT NULL DEFAULT '{}'::VARCHAR[], + fao_areas VARCHAR[] NOT NULL DEFAULT '{}'::VARCHAR[], + cgpm_areas VARCHAR[] NOT NULL DEFAULT '{}'::VARCHAR[], + gears VARCHAR[] NOT NULL DEFAULT '{}'::VARCHAR[], + flag_states VARCHAR[] NOT NULL DEFAULT '{}'::VARCHAR[], + minimum_quantity_kg DOUBLE PRECISION NOT NULL DEFAULT 0 +); From 1da018ea8d2c42c31a1fbb8411300095e4c450a5 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 14 Feb 2024 15:01:50 +0100 Subject: [PATCH 02/82] Add test data --- .../V666.27__Insert_dummy_pno_types.sql | 17 +++++++++++++++ .../V666.32__Insert_test_pno_types.sql | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql create mode 100644 datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql diff --git a/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql b/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql new file mode 100644 index 0000000000..e36aabffe9 --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql @@ -0,0 +1,17 @@ +INSERT INTO public.pno_types ( + id name, minimum_notification_period, has_designated_ports) VALUES + ( 1, 'Préavis type 1', 4, true), + ( 2, 'Préavis type 2', 4, true), + (10, 'Préavis par pavillon', 4, true), + (12, 'Préavis par engin', 4, true); + + +INSERT INTO public.pno_type_rules ( + pno_type_id, species, fao_areas, cgpm_areas, gears, flag_states, minimum_quantity_kg) VALUES + ( 1, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 0), + ( 1, '{HKE}', '{37}', '{30.01,30.05,30.06,30.07,30.09,30.10,30.11}', '{}', '{}', 0), + ( 1, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), + ( 2, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 2000), + ( 2, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), + ( 10, '{}', '{}', '{}', '{}', '{GBR,VEN}', 0), + ( 12, '{}', '{}', '{}', '{SB}', '{}', 0); \ No newline at end of file diff --git a/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql b/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql new file mode 100644 index 0000000000..bdcd6fb37f --- /dev/null +++ b/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql @@ -0,0 +1,21 @@ +DELETE FROM public.pno_type_rules; +DELETE FROM public.pno_types; + +INSERT INTO public.pno_types ( + id, name, minimum_notification_period, has_designated_ports) VALUES + ( 1, 'Préavis type 1', 4, true), + ( 2, 'Préavis type 2', 4, true), + ( 3, 'Préavis par pavillon', 4, true), + ( 4, 'Préavis par engin', 4, true); + +ALTER SEQUENCE pno_types_id_seq RESTART WITH 5; + +INSERT INTO public.pno_type_rules ( + pno_type_id, species, fao_areas, cgpm_areas, gears, flag_states, minimum_quantity_kg) VALUES + ( 1, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 0), + ( 1, '{HKE}', '{37}', '{30.01,30.05,30.06,30.07,30.09,30.10,30.11}', '{}', '{}', 0), + ( 1, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), + ( 2, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 2000), + ( 2, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), + ( 3, '{}', '{}', '{}', '{}', '{GBR,VEN}', 0), + ( 4, '{}', '{}', '{}', '{SB}', '{}', 0); \ No newline at end of file From 9cca201f243795409edc321b0e3c218616adf0e5 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 14 Feb 2024 15:02:16 +0100 Subject: [PATCH 03/82] Add init_pno_types flow --- .../src/pipeline/data/pno_type_rules.csv | 50 +++++++++ datascience/src/pipeline/data/pno_types.csv | 13 +++ .../src/pipeline/flows/init_pno_types.py | 106 ++++++++++++++++++ .../test_flows/test_init_pno_types.py | 42 +++++++ 4 files changed, 211 insertions(+) create mode 100644 datascience/src/pipeline/data/pno_type_rules.csv create mode 100644 datascience/src/pipeline/data/pno_types.csv create mode 100644 datascience/src/pipeline/flows/init_pno_types.py create mode 100644 datascience/tests/test_pipeline/test_flows/test_init_pno_types.py diff --git a/datascience/src/pipeline/data/pno_type_rules.csv b/datascience/src/pipeline/data/pno_type_rules.csv new file mode 100644 index 0000000000..ae1ced2d09 --- /dev/null +++ b/datascience/src/pipeline/data/pno_type_rules.csv @@ -0,0 +1,50 @@ +pno_type_id,species,fao_areas,cgpm_areas,gears,flag_states,minimum_quantity_kg +1,"['PZC', 'ALC', 'PHO', 'ANT', 'BSF', 'API', 'ARU', 'ALF', 'TVY', 'CWO', 'CFB', 'CYO', 'CYP', 'KEF', 'CMO', 'HXC', 'RNG', 'SCK', 'DCA', 'EPI', 'ETR', 'ETX', 'SHO', 'GAM', 'BRF', 'SBL', 'ORY', 'HPR', 'CYH', 'SFS', 'LKK', 'RTX', 'RHG', 'BLI', 'RIB', 'NNN', 'OXN', 'SBR', 'WRF', 'RJY', 'RJG', 'JAD', 'GHL', 'RCT', 'SYR', 'SFV', 'GSK', 'NEN', 'TJX']","['27.5.b', '27.6', '27.7', '27.8', '27.9', '27.10', '4.1.1', '34.1.2', '34.2']",[],[],[],0 +1,['BSF'],"['27.1', '27.2', '27.4', '27.6', '27.8', '27.10', '27.14', '27.3.a', '27.5.a', '27.5.b', '27.9.a', '27.12.b']",[],[],[],0 +1,['RNG'],"['27.6', '27.7', '27.5.b']",[],[],[],0 +1,['BSS'],"['27.4.b', '27.4.c', '27.6.a', '27.7.a', '27.7.b', '27.7.d', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.j', '27.8.a', '27.8.b', '27.8.c', '27.9.a']",[],[],[],0 +1,['COD'],"['27.7.a', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.i', '27.7.j', '27.7.k']",[],[],[],0 +1,['LEZ'],"['27.4.a', '27.6.a', '27.6.b', '27.7.b', '27.7.c', '27.7.d', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.i', '27.7.j', '27.7.k', '27.8.a', '27.8.b', '27.8.c', '27.8.d', '27.9.a']",[],[],[],0 +1,"['MNZ', 'ANF']","['27.7.b', '27.7.c', '27.7.d', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.i', '27.7.j', '27.7.k', '27.8.a', '27.8.b', '27.8.c', '27.8.d', '27.9.a']",[],[],[],0 +1,['HAD'],"['27.6.b', '27.7.a', '27.7.b', '27.7.c', '27.7.d', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.i', '27.7.j', '27.7.k']",[],[],[],0 +1,['WHG'],"['27.7.b', '27.7.c', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.i', '27.7.j', '27.7.k', '27.8', '27.9.a']",[],[],[],0 +1,['HKE'],"['27.3.a', '27.4', '27.6', '27.7', '27.8.a', '27.8.b', '27.8.c', '27.8.d', '27.9.a']",[],[],[],0 +1,['BLI'],"['27.5.b', '27.6', '27.7']",[],[],[],0 +1,['NEP'],"['27.5.b', '27.6', '27.7', '27.8.a', '27.8.b', '27.8.d', '27.8.e', '27.9', '27.10', '34.1.1']",[],[],[],0 +1,['SBR'],['27.9'],[],[],[],0 +1,['PLE'],"['27.7.d', '27.7.e']",[],[],[],0 +1,['SOL'],"['27.5', '27.12', '27.14', '27.6.b', '27.7.d', '27.7.e', '27.7.f', '27.7.g', '27.7.h', '27.7.j', '27.7.k', '27.8.a', '27.8.b', '27.8.c', '27.9.a']",[],[],[],0 +1,['POL'],"['27.6', '27.7']",[],[],[],0 +1,['COD'],"['27.4', '27.7.d', '27.3.a.20']",[],[],[],0 +1,['HAD'],"['27.4', '27.6.a', '27.3.a.20']",[],[],[],0 +1,['PLE'],"['27.4', '27.3.a.20']",[],[],[],0 +1,['POK'],"['27.4', '27.6', '27.3.a']",[],[],[],0 +1,['SOL'],"['27.4', '27.3.a']",[],[],[],0 +1,['WHG'],"['27.4', '27.7.d']",[],[],[],0 +1,"['MNZ', 'ANF']","['27.3.a', '27.4', '27.6']",[],[],[],0 +1,['PRA'],"['27.3.a.20', '27.4.a']",[],[],[],0 +1,['NEP'],"['27.4', '27.3.a']",[],[],[],0 +1,['ARA'],['37'],"['30.01', '30.05', '30.06', '30.07']",[],[],0 +1,['DPS'],['37'],"['30.01', '30.05', '30.06', '30.09', '30.10', '30.11']",[],[],0 +1,['ARS'],['37'],"['30.09', '30.10', '30.11']",[],[],0 +1,['HKE'],['37'],"['30.01', '30.05', '30.06', '30.07', '30.09', '30.10', '30.11']",[],[],0 +1,['NEP'],['37'],"['30.05', '30.06', '30.09', '30.11']",[],[],0 +1,['MUT'],['37'],"['30.01', '30.05', '30.06', '30.07', '30.09', '30.10', '30.11']",[],[],0 +1,['BFT'],"['27', '37']",[],[],[],0 +1,['SWO'],['37'],[],[],[],0 +1,['HER'],"['27.1', '27.2', '27.3.a', '27.4', '27.5.b', '27.6', '27.7']",[],[],[],10000 +1,['MAC'],"['27.2.a', '27.3.a', '27.4', '27.5.b', '27.6', '27.7', '27.8', '27.9', '27.12', '27.14', '34.1.2', '34.2']",[],[],[],10000 +1,['HOM'],"['27.2.a', '27.4', '27.5.b', '27.6', '27.7', '27.8', '27.9', '27.10', '27.12', '27.14', '34.1.2', '34.2']",[],[],[],10000 +1,['WHB'],"['27.2.a', '27.3.a', '27.4', '27.5.b', '27.6', '27.7', '27.8', '27.9', '27.10', '27.12', '27.14', '34.1.2', '34.2']",[],[],[],10000 +2,['COD'],"['27.4', '27.7.d', '27.3.a.20']",[],[],[],2000 +3,['HKE'],"['27.3.a', '27.4', '27.6', '27.7', '27.8.a', '27.8.b', '27.8.c', '27.8.d', '27.9.a']",[],[],[],2000 +4,"['PZC', 'ALC', 'PHO', 'ANT', 'BSF', 'API', 'ARU', 'ALF', 'TVY', 'CWO', 'CFB', 'CYO', 'CYP', 'KEF', 'CMO', 'HXC', 'RNG', 'SCK', 'DCA', 'EPI', 'ETR', 'ETX', 'SHO', 'GAM', 'BRF', 'SBL', 'ORY', 'HPR', 'CYH', 'SFS', 'LKK', 'RTX', 'RHG', 'BLI', 'RIB', 'NNN', 'OXN', 'SBR', 'WRF', 'RJY', 'RJG', 'JAD', 'GHL', 'RCT', 'SYR', 'SFV', 'GSK', 'NEN', 'TJX']","['27.5.b', '27.6', '27.7', '27.8', '27.9', '27.10', '4.1.1', '34.1.2', '34.2']",[],[],[],100 +5,"['HER', 'MAC', 'HOM', 'WHB']","['27', '34.1.2', '34.2']",[],[],[],10000 +6,['SOL'],"['27.4', '27.7.d', '27.7.e']",[],[],[],100 +6,['SOL'],"['27.8.a', '27.8.b']",[],[],[],50 +7,['BFT'],"['27', '37']",[],[],[],0 +8,['SWO'],['37'],[],[],[],0 +9,['COL'],['37'],[],[],[],0 +10,[],[],[],[],"['ABW', 'AFG', 'AGO', 'AIA', 'ALA', 'ALB', 'AND', 'ARE', 'ARG', 'ARM', 'ASM', 'ATA', 'ATF', 'ATG', 'AUS', 'AZE', 'BDI', 'BEN', 'BES', 'BFA', 'BGD', 'BHR', 'BHS', 'BIH', 'BLM', 'BLR', 'BLZ', 'BMU', 'BOL', 'BRA', 'BRB', 'BRN', 'BTN', 'BVT', 'BWA', 'CAF', 'CAN', 'CCK', 'CHE', 'CHL', 'CHN', 'CIV', 'CMR', 'COD', 'COG', 'COK', 'COL', 'COM', 'CPV', 'CRI', 'CUB', 'CUW', 'CXR', 'CYM', 'DJI', 'DMA', 'DOM', 'DZA', 'ECU', 'EGY', 'ERI', 'ESH', 'ETH', 'FJI', 'FLK', 'FRO', 'FSM', 'GAB', 'GBR', 'GEO', 'GGY', 'GHA', 'GIB', 'GIN', 'GLP', 'GMB', 'GNB', 'GNQ', 'GRD', 'GRL', 'GTM', 'GUM', 'GUY', 'HKG', 'HMD', 'HND', 'HTI', 'IDN', 'IMN', 'IND', 'IOT', 'IRN', 'IRQ', 'ISL', 'ISR', 'JAM', 'JEY', 'JOR', 'JPN', 'KAZ', 'KEN', 'KGZ', 'KHM', 'KIR', 'KNA', 'KOR', 'KWT', 'LAO', 'LBN', 'LBR', 'LBY', 'LCA', 'LIE', 'LKA', 'LSO', 'MAC', 'MAF', 'MAR', 'MCO', 'MDA', 'MDG', 'MDV', 'MEX', 'MHL', 'MKD', 'MLI', 'MMR', 'MNE', 'MNG', 'MNP', 'MOZ', 'MRT', 'MSR', 'MTQ', 'MUS', 'MWI', 'MYS', 'MYT', 'NAM', 'NCL', 'NER', 'NFK', 'NGA', 'NIC', 'NIU', 'NOR', 'NPL', 'NRU', 'NZL', 'OMN', 'PAK', 'PAN', 'PCN', 'PER', 'PHL', 'PLW', 'PNG', 'PRI', 'PRK', 'PRY', 'PSE', 'PYF', 'QAT', 'RUS', 'RWA', 'SAU', 'SDN', 'SEN', 'SGP', 'SGS', 'SHN', 'SJM', 'SLB', 'SLE', 'SLV', 'SMR', 'SOM', 'SPM', 'SRB', 'SSD', 'STP', 'SUR', 'SWZ', 'SXM', 'SYC', 'SYR', 'TCA', 'TCD', 'TGO', 'THA', 'TJK', 'TKL', 'TKM', 'TLS', 'TON', 'TTO', 'TUN', 'TUR', 'TUV', 'TWN', 'TZA', 'UGA', 'UKR', 'UMI', 'URY', 'USA', 'UZB', 'VAT', 'VCT', 'VEN', 'VGB', 'VIR', 'VNM', 'VUT', 'WLF', 'WSM', 'YEM', 'ZAF', 'ZMB', 'ZWE']",0 +11,[],[],[],[],"['AUT', 'BEL', 'BGR', 'HRV', 'CYP', 'CZE', 'DNK', 'EST', 'FIN', 'DEU', 'GRC', 'HUN', 'IRL', 'ITA', 'LVA', 'LTU', 'LUX', 'MLT', 'NLD', 'POL', 'PRT', 'ROU', 'SVK', 'SVN', 'ESP', 'SWE']",0 +12,[],[],[],['SB'],[],0 diff --git a/datascience/src/pipeline/data/pno_types.csv b/datascience/src/pipeline/data/pno_types.csv new file mode 100644 index 0000000000..fc33dc855a --- /dev/null +++ b/datascience/src/pipeline/data/pno_types.csv @@ -0,0 +1,13 @@ +id,name,minimum_notification_period,has_designated_ports +1,Espèces soumises à préavis,4.0,True +2,Ports désignés cabillaud,4.0,True +3,Ports désignés merlu,4.0,True +4,Ports désignés espèces eaux profondes,4.0,True +5,Ports désignés petits pélagiques,4.0,True +6,Ports désignés sole,4.0,True +7,Ports désignés thon rouge,4.0,True +8,Ports désignés espadon,4.0,True +9,Ports désignés corail rouge,4.0,True +10,Préavis navire tiers,4.0,True +11,Préavis communautaire,4.0,True +12,Préavis senne de plage,4.0,True diff --git a/datascience/src/pipeline/flows/init_pno_types.py b/datascience/src/pipeline/flows/init_pno_types.py new file mode 100644 index 0000000000..fced99cd80 --- /dev/null +++ b/datascience/src/pipeline/flows/init_pno_types.py @@ -0,0 +1,106 @@ +from ast import literal_eval +from pathlib import Path + +import pandas as pd +import prefect +from prefect import Flow, case, task +from prefect.executors import LocalDaskExecutor +from sqlalchemy import DDL, Table + +from config import LIBRARY_LOCATION +from src.db_config import create_engine +from src.pipeline.generic_tasks import load +from src.pipeline.shared_tasks.control_flow import check_flow_not_running +from src.pipeline.shared_tasks.infrastructure import get_table +from src.pipeline.utils import delete + + +@task(checkpoint=False) +def extract_pno_types(): + return pd.read_csv( + LIBRARY_LOCATION / "pipeline/data/pno_types.csv", + encoding="utf8", + dtype={"has_designated_ports": bool}, + ) + + +@task(checkpoint=False) +def extract_pno_type_rules(): + return pd.read_csv( + LIBRARY_LOCATION / "pipeline/data/pno_type_rules.csv", + encoding="utf8", + converters=dict.fromkeys( + ["species", "fao_areas", "cgpm_areas", "gears", "flag_states"], literal_eval + ), + ) + + +@task(checkpoint=False) +def load_pno_types_and_rules( + pno_types: pd.DataFrame, + pno_type_rules: pd.DataFrame, + pno_types_table: Table, + pno_type_rules_table: Table, +): + logger = prefect.context.get("logger") + + e = create_engine("monitorfish_remote") + + with e.begin() as con: + delete(table=pno_type_rules_table, connection=con, logger=logger) + delete(table=pno_types_table, connection=con, logger=logger) + + load( + pno_types, + table_name="pno_types", + schema="public", + connection=con, + logger=prefect.context.get("logger"), + how="replace", + end_ddls=[ + DDL( + "SELECT setval(" + "pg_get_serial_sequence('pno_types', 'id'), " + "coalesce(max(id),0) + 1, false" + ") " + "FROM pno_types;" + ) + ], + ) + + load( + pno_type_rules, + table_name="pno_type_rules", + schema="public", + connection=con, + logger=prefect.context.get("logger"), + how="replace", + pg_array_columns=[ + "species", + "fao_areas", + "cgpm_areas", + "gears", + "flag_states", + ], + init_ddls=[ + DDL( + "SELECT setval(" + "pg_get_serial_sequence('pno_type_rules', 'id'), 1, false" + ")" + ) + ], + ) + + +with Flow("Init pno types", executor=LocalDaskExecutor()) as flow: + flow_not_running = check_flow_not_running() + with case(flow_not_running, True): + pno_types_table = get_table("pno_types") + pno_type_rules_table = get_table("pno_type_rules") + pno_types = extract_pno_types() + pno_type_rules = extract_pno_type_rules() + load_pno_types_and_rules( + pno_types, pno_type_rules, pno_types_table, pno_type_rules_table + ) + +flow.file_name = Path(__file__).name diff --git a/datascience/tests/test_pipeline/test_flows/test_init_pno_types.py b/datascience/tests/test_pipeline/test_flows/test_init_pno_types.py new file mode 100644 index 0000000000..7d5231d745 --- /dev/null +++ b/datascience/tests/test_pipeline/test_flows/test_init_pno_types.py @@ -0,0 +1,42 @@ +import pandas as pd + +from src.pipeline.flows.init_pno_types import flow +from src.read_query import read_query +from tests.mocks import mock_check_flow_not_running + +flow.replace(flow.get_tasks("check_flow_not_running")[0], mock_check_flow_not_running) + + +def test_flow(reset_test_data): + pno_types_query = "SELECT * FROM pno_types ORDER BY id" + pno_type_rules_query = "SELECT * FROM pno_type_rules ORDER BY id" + initial_pno_types = read_query(pno_types_query, db="monitorfish_remote") + initial_pno_type_rules = read_query(pno_type_rules_query, db="monitorfish_remote") + + flow.schedule = None + state = flow.run() + assert state.is_successful() + + pno_types_after_first_run = read_query(pno_types_query, db="monitorfish_remote") + pno_type_rules_after_first_run = read_query( + pno_type_rules_query, db="monitorfish_remote" + ) + + assert len(initial_pno_types) == 4 + assert len(pno_types_after_first_run) == 12 + assert len(initial_pno_type_rules) == 7 + assert len(pno_type_rules_after_first_run) == 49 + + # Re-running should succeed and lead to the same pno types + state = flow.run() + assert state.is_successful() + + pno_types_after_second_run = read_query(pno_types_query, db="monitorfish_remote") + pno_type_rules_after_second_run = read_query( + pno_type_rules_query, db="monitorfish_remote" + ) + + pd.testing.assert_frame_equal(pno_types_after_first_run, pno_types_after_second_run) + pd.testing.assert_frame_equal( + pno_type_rules_after_first_run, pno_type_rules_after_second_run + ) From 7c3eaef71555b0ec9c8a269c825fb333f36f8895 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 14 Feb 2024 15:03:37 +0100 Subject: [PATCH 04/82] Register init_pno_types flow --- datascience/src/pipeline/flows_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/datascience/src/pipeline/flows_config.py b/datascience/src/pipeline/flows_config.py index 7e35b6a46f..50a5942b1e 100644 --- a/datascience/src/pipeline/flows_config.py +++ b/datascience/src/pipeline/flows_config.py @@ -35,6 +35,7 @@ fishing_gear_codes, foreign_fmcs, infractions, + init_pno_types, init_species_groups, last_positions, logbook, @@ -272,6 +273,7 @@ fishing_gear_codes.flow, foreign_fmcs.flow, infractions.flow, + init_pno_types.flow, init_species_groups.flow, last_positions.flow, missing_far_alerts.flow, From a0ad96d962016b520d72156e09d715f8ca6fe55e Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 16 Feb 2024 15:06:13 +0100 Subject: [PATCH 05/82] Extract predicted landing datetime from PNOs --- .../internal/V0.245__Update_logbook_reports_table.sql | 5 +++++ datascience/src/pipeline/parsers/ers/log_parsers.py | 5 +++++ datascience/tests/test_pipeline/test_parsers/test_ers.py | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql diff --git a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql new file mode 100644 index 0000000000..bc3d1fc872 --- /dev/null +++ b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql @@ -0,0 +1,5 @@ +ALTER TABLE public.logbook_reports + ADD COLUMN enriched BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN trip_gears jsonb, + ADD COLUMN pno_types jsonb, + ADD COLUMN trip_segments VARCHAR[]; diff --git a/datascience/src/pipeline/parsers/ers/log_parsers.py b/datascience/src/pipeline/parsers/ers/log_parsers.py index c7a5de7343..c810a02dcd 100644 --- a/datascience/src/pipeline/parsers/ers/log_parsers.py +++ b/datascience/src/pipeline/parsers/ers/log_parsers.py @@ -219,6 +219,10 @@ def parse_pno(pno): time = pno.get("PT") predicted_arrival_datetime_utc = make_datetime_json_serializable(date, time) + date = pno.get("DA") + time = pno.get("TI") + predicted_landing_datetime_utc = make_datetime_json_serializable(date, time) + start_date = pno.get("DS") trip_start_date = make_datetime_json_serializable(start_date, None) @@ -226,6 +230,7 @@ def parse_pno(pno): value = { "predictedArrivalDatetimeUtc": predicted_arrival_datetime_utc, + "predictedLandingDatetimeUtc": predicted_landing_datetime_utc, "port": pno.get("PO"), "purpose": pno.get("PC"), "tripStartDate": trip_start_date, diff --git a/datascience/tests/test_pipeline/test_parsers/test_ers.py b/datascience/tests/test_pipeline/test_parsers/test_ers.py index 04fc18db00..b7fd2dadbe 100644 --- a/datascience/tests/test_pipeline/test_parsers/test_ers.py +++ b/datascience/tests/test_pipeline/test_parsers/test_ers.py @@ -303,11 +303,13 @@ def test_pno_parser(): assert set(value) == { "catchOnboard", "predictedArrivalDatetimeUtc", + "predictedLandingDatetimeUtc", "tripStartDate", "port", "purpose", } assert value["predictedArrivalDatetimeUtc"] == "2020-03-26T12:44:00Z" + assert value["predictedLandingDatetimeUtc"] == "2020-03-26T12:44:00Z" assert value["port"] == "FRQUI" assert value["purpose"] == "LAN" assert value["tripStartDate"] == "2020-03-26T00:00:00Z" From 026e3908ffa37b66cc2002ab5e3c52fae6f4c6fc Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 27 Feb 2024 18:08:17 +0100 Subject: [PATCH 06/82] Add ADR for prior notifications and pno types --- ...r-notifications-architecture-and-typing.md | 94 ++++++++++++++++++ adrs/images/pno_types_architecture.png | Bin 0 -> 119431 bytes 2 files changed, 94 insertions(+) create mode 100644 adrs/0004-prior-notifications-architecture-and-typing.md create mode 100644 adrs/images/pno_types_architecture.png diff --git a/adrs/0004-prior-notifications-architecture-and-typing.md b/adrs/0004-prior-notifications-architecture-and-typing.md new file mode 100644 index 0000000000..3f0d8a3b2e --- /dev/null +++ b/adrs/0004-prior-notifications-architecture-and-typing.md @@ -0,0 +1,94 @@ +# Architecture de données et typage des préavis + +Date: 27/02/2024 + +## Statut + +Résolu. + +## Contexte + +Le développement de la brique de gestion des préavis dans Monitorfish, en remplacement de Trident qui assurait +jusqu'ici cette gestion, nous amène à définir une **architecture de données** pour leur stockage en base de données +ainsi qu'une **stratégie de typage** permettant la catégorisation de ces préavis à des fins de lecture opérationnelle +rapide des préavis entrants. + +**Concernant l'architecture** : les données provenant de deux sources différentes (logbook pour les préavis des navires +de plus de 12 m, saisie dans Monitorfish pour les navires de moins de 12 m), il apparait naturel de stocker ces +données séparément dans une table correspondant à chaque source : +- la table `logbook_reports` déjà existante contient déjà les données de préavis qu'il s'agit ici d'exploiter pour les + navires >= 12m +- une nouvelle table `manual_pnos` pour les préavis saisis dans Monitorfish + +Le choix qui reste à faire concerne le mode de stockage de l'information de type : en effet, les critères de typage +(espèces, quantités...) correspondant à chaque type de préavis pouvant évoluer dans le temps, le format de stockage de +la donnée de type doit être défini en ayant en tête cette variabilité et les questions d'historisation qui en +découlent. + +**Concernant le typage** : plusieurs contraintes rendent la tâche de typage non triviale : + +- Les données sont entrées à des **temporalités différentes** (batch chaque minute pour les données logbook, temps réel + au moment de la saisie pour les préavis saisis dans l'app) +- Les données sont écrites en base par des **composants différents** : un `flow` pour les données issues de logbook, + le backend pour les données saisies dans l'app + +Il faut donc définir quel(s) composant(s) (pipeline et / ou backend) ont la responsabilité du typage, et à quel moment +(précalcul avec stockage de l'information de type de chaque préavis, ou calcul à la volée au moment de la requête, sans + stockage). + +#### Stratégie #1 : typage précalculé en batch de tous les PNOs par la pipeline + +Dans cette stratégie, c'est un `flow` de la pipeline qui ajoute l'information de type à la fois dans les PNO manuels et +dans les PNO issus de logbook. + +La problématique d'historisation est traitée en stockant dans chaque PNO les informations liées au type. + +Avantages : +- segments des marées passées disponibles +- performance en requête sur des préavis déjà typés +- réutilisation du code de typage par la pipeline pour les deux sources de données + +Inconvénients : +- gestion d'un état transitoire en front entre le moment où un préavis est saisi et le moment où la pipeline lui a + attribué un type +- complexité à gérer en cas de modification d'un préavis manuel : il faut signaler à la pipeline qu'il faut recalculer + le type du préavis. De ce fait, une incertitude existe à tout moment sur l'exactitude de l'information de type des + préavis manuels. + +#### Option 2 : typage à la volée par le backend au moment de la requête + +Dans cette stratégie, c'est au moment de la requête que les types de préavis sont calculés par le back. + +Avantages : +- réutilisation du code de typage par le backend pour les deux sources de données +- pas de problème de latence entre saisie d'un préavis et attribution d'un type + +Inconvénients : +- performances possiblement faibles sur des requêtes de larges plages de dates +- segments de flotte non disponibles sur préavis passés (uniquement segments de la marée en cours) + +#### Option 3 : typage précalculé par la pipeline (données logbook) et par le backend (préavis manuels) + +Dans cette stratégie : +- un `flow` de la pipeline ajoute l'information de type dans les PNO issus de logbook. +- le backend ajoute l'information de type dans les PNO manuels au moment de la saisie + +Avantages : +- segments des marées passées disponibles +- performance en requête sur des préavis déjà typés +- pas de problème de latence entre saisie d'un préavis et attribution d'un type + +Inconvénients : +- logique de typage à implémenter à la fois en backend et dans la pipeline + +## Décision + +La stratégie n°3 a été choisie afin d'avoir à la fois la performance y compris sur de larges volumes de données +passées, la disponibilité des données de segments des marées passées et éviter les problématiques de latence liées à la stratégie n°1. + +![Vue schématique des tables et flux de données de préavis](images/pno_types_architecture.png "Architecture des préavis et de leur types"). + +## Conséquences + +Il faudra tester exhaustivement les logiques de typage dans le backend et dans la pipeline pour s'assurer qu'elles +sont identiques. \ No newline at end of file diff --git a/adrs/images/pno_types_architecture.png b/adrs/images/pno_types_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..3640bbe697160e253427d047b49ad570e716bde7 GIT binary patch literal 119431 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 Mt)*88&&vvZXh!bs81XIk-$?`snoh3Cs5MK}AU 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 Zjfox2*1*?RI?B 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 IQN!2&2S#SdhyVZp literal 0 HcmV?d00001 From 27ac5c7a9bfbc744ba4e2434c8d0846ed797192d Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 29 Feb 2024 15:34:31 +0100 Subject: [PATCH 07/82] Lint --- .../V666.5__Reset_test_logbook.sql | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql b/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql index f312b7238e..5bfb46fe74 100644 --- a/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql +++ b/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql @@ -126,29 +126,30 @@ WHERE operation_number = '5'; -- Add FLUX test data -INSERT INTO logbook_reports (operation_number, operation_country, operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, value, integration_datetime_utc, trip_number, transmission_format) VALUES -('cc7ee632-e515-460f-a1c1-f82222a6d419',null,'2020-05-06 18:40:51','DAT','f006a2e5-0fdd-48a0-9a9a-ccae00d052d8',null,'2020-05-06 15:40:51','SOCR4T3','IRCS6','XR006','GOLF','CYP','1234567','NOT_COX','{"faoZoneExited": null, "latitudeExited": 57.7258, "longitudeExited": 0.5983, "effortZoneExited": null, "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:51.795Z", "statisticalRectangleExited": null}','2022-03-31 09:21:19.378408','SRC-TRP-TTT20200506194051795','FLUX'), -('a3c52754-97e1-4a21-ba2e-d8f16f4544e9',null,'2020-05-06 18:40:57','DAT','9d1ddd34-1394-470e-b8a6-469b86150e1e',null,'2020-05-06 15:40:57','SOCR4T3',null,null,null,'CYP',null,'COX','{"faoZoneExited": null, "latitudeExited": 46.678, "longitudeExited": -14.616, "effortZoneExited": "A", "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:57.580Z", "statisticalRectangleExited": null}','2022-03-31 09:21:19.384086','SRC-TRP-TTT20200506194051795','FLUX'), -('d5c3b039-aaee-4cca-bcae-637fa8effe14',null,'2020-05-06 18:41:03','DAT','7ee30c6c-adf9-4f60-a4f1-f7f15ab92803',null,'2020-05-06 15:41:03','SOCR4T3',null,null,null,'CYP',null,'PNO','{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z"}','2022-03-31 09:21:19.38991','SRC-TRP-TTT20200506194051795','FLUX'), -('7cfcdde3-286c-4713-8460-2ed82a59be34',null,'2020-05-06 18:41:09','DAT','fc16ea8a-3148-44b2-977f-de2a2ae550b9',null,'2020-05-06 15:41:09','SOCR4T3',null,null,null,'CYP',null,'PNO','{"port": "GBPHD", "purpose": "SHE", "tripStartDate": "2020-05-04T19:41:09.200Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:09.200Z"}','2022-03-31 09:21:19.395805','SRC-TRP-TTT20200506194051795','FLUX'), -('4f971076-e6c6-48f6-b87e-deae90fe4705',null,'2020-05-06 18:41:15','DAT','cc45063f-2d3c-4cda-ac0c-8381e279e150',null,'2020-05-06 15:41:15','SOCR4T3',null,null,'GOLF','CYP',null,'RTP','{"port": "ESCAR", "reasonOfReturn": "REF", "returnDatetimeUtc": "2020-05-06T11:41:15.013Z"}','2022-03-31 09:21:19.401686','SRC-TRP-TTT20200506194051795','FLUX'), -('8f06061e-e723-4b89-8577-3801a61582a2',null,'2020-05-06 18:41:20','DAT','dde5df56-24c2-4a2e-8afb-561f32113256',null,'2020-05-06 15:41:20','SOCR4T3','IRCS6','XR006',null,'CYP',null,'RTP','{"port": "ESCAR", "gearOnboard": [{"gear": "GN", "mesh": 140.0, "dimensions": 1000.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2020-05-06T11:41:20.712Z"}','2022-03-31 09:21:19.407777','SRC-TRP-TTT20200506194051795','FLUX'), -('8db132d1-68fc-4ae6-b12e-4af594351701',null,'2020-05-06 18:41:26','DAT','83952732-ef89-4168-b2a1-df49d0aa1aff',null,'2020-05-06 15:41:26','SOCR4T3',null,null,null,'CYP',null,'LAN','{"port": "ESCAR", "sender": null, "catchLanded": [{"nbFish": null, "weight": 100.0, "faoZone": "27.9.b.2", "species": "HAD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.2, "preservationState": "FRO", "statisticalRectangle": null}], "landingDatetimeUtc": "2020-05-05T19:41:26.516Z"}','2022-03-31 09:21:19.414081','SRC-TRP-TTT20200506194051795','FLUX'), -('b509d82f-ce27-46c2-b5a3-d2bcae09de8a',null,'2020-05-06 18:41:32','DAT','ddf8f969-86f1-4eb9-a9a6-d61067a846bf',null,'2020-05-06 15:41:32','SOCR4T3',null,null,null,'SVN',null,'TRA','null','2022-03-31 09:21:19.420333','SRC-TRP-TTT20200506194051795','FLUX'), -('6c26236d-51ad-4aee-ac37-8e83978346a0',null,'2020-05-06 18:41:38','DAT','b581876a-ae95-4a07-8b56-b6b5d8098a57',null,'2020-05-06 15:41:38','SOCR4T3',null,null,null,'SVN',null,'TRA','null','2022-03-31 09:21:19.426686','SRC-TRP-TTT20200506194051795','FLUX'), -('81cf0182-db9c-4384-aca3-a75b1067c41a',null,'2020-05-06 18:41:43','DAT','ce5c46ca-3912-4de1-931c-d66b801b5362',null,'2020-05-06 15:41:43','SOCR4T3',null,null,null,'CYP',null,'NOT_TRA','null','2022-03-31 09:21:19.433052','SRC-TRP-TTT20200506194051795','FLUX'), -('ab1058c7-b7cf-4345-a0b3-a9f472cc6ef6',null,'2020-05-06 18:41:49','DAT','e43c3bf0-163c-4fb0-a1de-1a61beb87988',null,'2020-05-06 15:41:49','SOCR4T3','IRCS6','XR006',null,'CYP','1234567','NOT_TRA','null','2022-03-31 09:21:19.439501','SRC-TRP-TTT20200506194051795','FLUX'), -('8826952f-b240-4570-a9dc-59f3a24c7bf1',null,'2020-05-06 18:39:33','DAT','1e1bff95-dfff-4cc3-82d3-d72b46fda745',null,'2020-05-06 15:39:33','SOCR4T3',null,null,'GOLF','CYP','1234567','DEP','{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "speciesOnboard": [{"nbFish": null, "weight": 50.0, "faoZone": "27.9.b.2", "species": "COD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.1, "preservationState": "FRO", "statisticalRectangle": null}], "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:33.176Z"}','2022-03-31 09:21:19.501868','SRC-TRP-TTT20200506194051795','FLUX'), -('5ee8be46-2efe-4a29-b2df-bdf2d3ed66a1',null,'2020-05-06 18:39:40','DAT','7712fe73-cef2-4646-97bb-d634fde00b07',null,'2020-05-06 15:39:40','SOCR4T3',null,null,'GOLF','CYP','1234567','DEP','{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:40.722Z"}','2022-03-31 09:21:19.507524','SRC-TRP-TTT20200506194051795','FLUX'), -('48794a8f-adfa-43b2-b4c3-2e8d3581bfb4',null,'2020-05-06 18:39:46','DAT','2843bd5b-e4e7-4816-8372-76805201301e',null,'2020-05-06 15:39:46','SOCR4T3','IRCS6','XR006','GOLF','CYP','1234567','NOT_COE','{"latitudeEntered": 42.794, "longitudeEntered": -13.809, "faoZoneEntered": null, "effortZoneEntered": null, "economicZoneEntered": null, "targetSpeciesOnEntry": null, "effortZoneEntryDatetimeUtc": "2020-05-06T11:39:46.583Z", "statisticalRectangleEntered": null}','2022-03-31 09:21:19.513305','SRC-TRP-TTT20200506194051795','FLUX'), -('196aca16-da66-4077-b340-ecad701be662',null,'2020-05-06 18:39:59','DAT','b2fca5fb-d1cd-4ec7-8a8c-645cecab6866',null,'2020-05-06 15:39:59','SOCR4T3',null,null,null,'CYP',null,'FAR','{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-06T11:39:59.462Z"}]}','2022-03-31 09:21:19.519424','SRC-TRP-TTT20200506194051795','FLUX'), -('4a4c8d24-f4be-4ccb-8aef-99ab5aae7e02',null,'2020-05-06 18:40:05','DAT','1a87f3de-dea9-4018-8c2e-d6cdfa97318e',null,'2020-05-06 15:40:05','SOCR4T3','IRCS6','XR006','GOLF','CYP','1234567','FAR','{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}, {"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 600.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D6"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}]}','2022-03-31 09:21:19.525832','SRC-TRP-TTT20200506194051795','FLUX'), -('251db84c-1d8b-49be-b426-f70bb2c68a2d',null,'2020-05-06 18:40:11','DAT','fe7acdb9-ff2e-4cfa-91a9-fd2e06b556e1',null,'2020-05-06 15:40:11','SOCR4T3',null,null,null,'CYP',null,'FAR','{"hauls": [{"farDatetimeUtc": "2020-05-06T11:40:11.291Z"}]}','2022-03-31 09:21:19.531881','SRC-TRP-TTT20200506194051795','FLUX'), -('08a125d6-6b6d-4f90-b26a-bf8426673eea',null,'2020-05-06 18:40:17','DAT','74fcd0f7-8117-4791-9aa3-37d5c7dce880',null,'2020-05-06 15:40:17','SOCR4T3',null,null,null,'SVN',null,'FAR','{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "species": "BFT"}], "latitude": 39.65, "longitude": 6.83, "farDatetimeUtc": "2020-04-29T12:00:00.000Z"}]}','2022-03-31 09:21:19.538061','SRC-TRP-TTT20200506194051795','FLUX'), -('9e38840b-f05a-49a4-ab34-e41131749fd0',null,'2020-05-06 18:40:22','DAT','1706938b-c3c8-4d34-b32f-54c8d2c0705a',null,'2020-05-06 15:40:22','SOCR4T3','IRCS6','XR006','GOLF','CYP','1234567','FAR','{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "faoZone": "27.8.e.1", "species": "MZZ", "effortZone": null, "economicZone": null, "statisticalRectangle": null}], "farDatetimeUtc": "2020-05-06T11:40:22.885Z"}]}','2022-03-31 09:21:19.544336','SRC-TRP-TTT20200506194051795','FLUX'), -('60e0d2e0-2713-43d7-9fa1-fcf968e34d82',null,'2020-05-06 18:40:28','DAT','a36d23c5-b339-455d-9b0b-bf766a9d57d9',null,'2020-05-06 15:40:28','SOCR4T3','IRCS6','XR006','GOLF','CYP','1234567','JFO','null','2022-03-31 09:21:19.550891','SRC-TRP-TTT20200506194051795','FLUX'), -('0e1ea2b6-f4f5-4958-bc48-cfb016a22f58',null,'2020-05-06 18:40:34','DAT','a913a52e-5e66-4f40-8c64-148f90fa8cd9',null,'2020-05-06 15:40:34','SOCR4T3',null,null,null,'CYP',null,'DIS','{"catches": [{"nbFish": null, "weight": 100.0, "species": "COD"}], "discardDatetimeUtc": "2020-05-06T11:40:34.449Z"}','2022-03-31 09:21:19.557299','SRC-TRP-TTT20200506194051795','FLUX'), -('3cffa378-0f8c-4540-b849-747621cfcb4a',null,'2020-05-06 18:40:40','DAT','7b487ada-019c-4b62-be32-7d15f7718344',null,'2020-05-06 15:40:40','SOCR4T3',null,null,null,'CYP','1234567','RLC','null','2022-03-31 09:21:19.563768','SRC-TRP-TTT20200506194051795','FLUX'), -('7bf7401d-cbb1-4e6f-bad8-7e309ee004cf',null,'2020-05-06 18:40:45','DAT','ced42f65-a1ac-40e1-93c7-851d4933f770',null,'2020-05-06 15:40:45','SOCR4T3',null,null,'GOLF','CYP',null,'RLC','null','2022-03-31 09:21:19.570417','SRC-TRP-TTT20200506194051795','FLUX'), -('9376ccbd-be2f-4d3d-b4ac-3c559ac9586a',null,'2021-01-31 12:29:02','DAT','8eec0190-c353-4147-8a65-fcc697fbadbc',null,'2021-01-22 09:02:47','SOCR4T3','OPUF','Z.510','Dennis','BEL',null,'COE','{"latitudeEntered": 51.333333, "longitudeEntered": 3.2, "faoZoneEntered": "27.4.c", "effortZoneEntered": null, "economicZoneEntered": "BEL", "targetSpeciesOnEntry": "DEMERSAL", "effortZoneEntryDatetimeUtc": "2021-01-22T09:00:00Z", "statisticalRectangleEntered": "31F3"}','2022-03-31 09:21:19.496049','SRC-TRP-TTT20200506194051795','FLUX'); +INSERT INTO logbook_reports ( + operation_number, operation_country, operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, trip_number, transmission_format, integration_datetime_utc, value) VALUES +('cc7ee632-e515-460f-a1c1-f82222a6d419', null, '2020-05-06 18:40:51', 'DAT', 'f006a2e5-0fdd-48a0-9a9a-ccae00d052d8', null, '2020-05-06 15:40:51', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'NOT_COX', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.378408', '{"faoZoneExited": null, "latitudeExited": 57.7258, "longitudeExited": 0.5983, "effortZoneExited": null, "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:51.795Z", "statisticalRectangleExited": null}'), +('a3c52754-97e1-4a21-ba2e-d8f16f4544e9', null, '2020-05-06 18:40:57', 'DAT', '9d1ddd34-1394-470e-b8a6-469b86150e1e', null, '2020-05-06 15:40:57', 'SOCR4T3', null, null, null, 'CYP', null, 'COX', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.384086', '{"faoZoneExited": null, "latitudeExited": 46.678, "longitudeExited": -14.616, "effortZoneExited": "A", "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:57.580Z", "statisticalRectangleExited": null}'), +('d5c3b039-aaee-4cca-bcae-637fa8effe14', null, '2020-05-06 18:41:03', 'DAT', '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.38991' , '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z"}'), +('7cfcdde3-286c-4713-8460-2ed82a59be34', null, '2020-05-06 18:41:09', 'DAT', 'fc16ea8a-3148-44b2-977f-de2a2ae550b9', null, '2020-05-06 15:41:09', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.395805', '{"port": "GBPHD", "purpose": "SHE", "tripStartDate": "2020-05-04T19:41:09.200Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:09.200Z"}'), +('4f971076-e6c6-48f6-b87e-deae90fe4705', null, '2020-05-06 18:41:15', 'DAT', 'cc45063f-2d3c-4cda-ac0c-8381e279e150', null, '2020-05-06 15:41:15', 'SOCR4T3', null, null, 'GOLF', 'CYP', null, 'RTP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.401686', '{"port": "ESCAR", "reasonOfReturn": "REF", "returnDatetimeUtc": "2020-05-06T11:41:15.013Z"}'), +('8f06061e-e723-4b89-8577-3801a61582a2', null, '2020-05-06 18:41:20', 'DAT', 'dde5df56-24c2-4a2e-8afb-561f32113256', null, '2020-05-06 15:41:20', 'SOCR4T3', 'IRCS6', 'XR006', null, 'CYP', null, 'RTP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.407777', '{"port": "ESCAR", "gearOnboard": [{"gear": "GN", "mesh": 140.0, "dimensions": 1000.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2020-05-06T11:41:20.712Z"}'), +('8db132d1-68fc-4ae6-b12e-4af594351701', null, '2020-05-06 18:41:26', 'DAT', '83952732-ef89-4168-b2a1-df49d0aa1aff', null, '2020-05-06 15:41:26', 'SOCR4T3', null, null, null, 'CYP', null, 'LAN', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.414081', '{"port": "ESCAR", "sender": null, "catchLanded": [{"nbFish": null, "weight": 100.0, "faoZone": "27.9.b.2", "species": "HAD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.2, "preservationState": "FRO", "statisticalRectangle": null}], "landingDatetimeUtc": "2020-05-05T19:41:26.516Z"}'), +('b509d82f-ce27-46c2-b5a3-d2bcae09de8a', null, '2020-05-06 18:41:32', 'DAT', 'ddf8f969-86f1-4eb9-a9a6-d61067a846bf', null, '2020-05-06 15:41:32', 'SOCR4T3', null, null, null, 'SVN', null, 'TRA', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.420333', 'null'), +('6c26236d-51ad-4aee-ac37-8e83978346a0', null, '2020-05-06 18:41:38', 'DAT', 'b581876a-ae95-4a07-8b56-b6b5d8098a57', null, '2020-05-06 15:41:38', 'SOCR4T3', null, null, null, 'SVN', null, 'TRA', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.426686', 'null'), +('81cf0182-db9c-4384-aca3-a75b1067c41a', null, '2020-05-06 18:41:43', 'DAT', 'ce5c46ca-3912-4de1-931c-d66b801b5362', null, '2020-05-06 15:41:43', 'SOCR4T3', null, null, null, 'CYP', null, 'NOT_TRA', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.433052', 'null'), +('ab1058c7-b7cf-4345-a0b3-a9f472cc6ef6', null, '2020-05-06 18:41:49', 'DAT', 'e43c3bf0-163c-4fb0-a1de-1a61beb87988', null, '2020-05-06 15:41:49', 'SOCR4T3', 'IRCS6', 'XR006', null, 'CYP', '1234567', 'NOT_TRA', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.439501', 'null'), +('8826952f-b240-4570-a9dc-59f3a24c7bf1', null, '2020-05-06 18:39:33', 'DAT', '1e1bff95-dfff-4cc3-82d3-d72b46fda745', null, '2020-05-06 15:39:33', 'SOCR4T3', null, null, 'GOLF', 'CYP', '1234567', 'DEP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.501868', '{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "speciesOnboard": [{"nbFish": null, "weight": 50.0, "faoZone": "27.9.b.2", "species": "COD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.1, "preservationState": "FRO", "statisticalRectangle": null}], "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:33.176Z"}'), +('5ee8be46-2efe-4a29-b2df-bdf2d3ed66a1', null, '2020-05-06 18:39:40', 'DAT', '7712fe73-cef2-4646-97bb-d634fde00b07', null, '2020-05-06 15:39:40', 'SOCR4T3', null, null, 'GOLF', 'CYP', '1234567', 'DEP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.507524', '{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:40.722Z"}'), +('48794a8f-adfa-43b2-b4c3-2e8d3581bfb4', null, '2020-05-06 18:39:46', 'DAT', '2843bd5b-e4e7-4816-8372-76805201301e', null, '2020-05-06 15:39:46', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'NOT_COE', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.513305', '{"latitudeEntered": 42.794, "longitudeEntered": -13.809, "faoZoneEntered": null, "effortZoneEntered": null, "economicZoneEntered": null, "targetSpeciesOnEntry": null, "effortZoneEntryDatetimeUtc": "2020-05-06T11:39:46.583Z", "statisticalRectangleEntered": null}'), +('196aca16-da66-4077-b340-ecad701be662', null, '2020-05-06 18:39:59', 'DAT', 'b2fca5fb-d1cd-4ec7-8a8c-645cecab6866', null, '2020-05-06 15:39:59', 'SOCR4T3', null, null, null, 'CYP', null, 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.519424', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-06T11:39:59.462Z"}]}'), +('4a4c8d24-f4be-4ccb-8aef-99ab5aae7e02', null, '2020-05-06 18:40:05', 'DAT', '1a87f3de-dea9-4018-8c2e-d6cdfa97318e', null, '2020-05-06 15:40:05', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.525832', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}, {"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 600.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D6"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}]}'), +('251db84c-1d8b-49be-b426-f70bb2c68a2d', null, '2020-05-06 18:40:11', 'DAT', 'fe7acdb9-ff2e-4cfa-91a9-fd2e06b556e1', null, '2020-05-06 15:40:11', 'SOCR4T3', null, null, null, 'CYP', null, 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.531881', '{"hauls": [{"farDatetimeUtc": "2020-05-06T11:40:11.291Z"}]}'), +('08a125d6-6b6d-4f90-b26a-bf8426673eea', null, '2020-05-06 18:40:17', 'DAT', '74fcd0f7-8117-4791-9aa3-37d5c7dce880', null, '2020-05-06 15:40:17', 'SOCR4T3', null, null, null, 'SVN', null, 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.538061', '{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "species": "BFT"}], "latitude": 39.65, "longitude": 6.83, "farDatetimeUtc": "2020-04-29T12:00:00.000Z"}]}'), +('9e38840b-f05a-49a4-ab34-e41131749fd0', null, '2020-05-06 18:40:22', 'DAT', '1706938b-c3c8-4d34-b32f-54c8d2c0705a', null, '2020-05-06 15:40:22', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.544336', '{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "faoZone": "27.8.e.1", "species": "MZZ", "effortZone": null, "economicZone": null, "statisticalRectangle": null}], "farDatetimeUtc": "2020-05-06T11:40:22.885Z"}]}'), +('60e0d2e0-2713-43d7-9fa1-fcf968e34d82', null, '2020-05-06 18:40:28', 'DAT', 'a36d23c5-b339-455d-9b0b-bf766a9d57d9', null, '2020-05-06 15:40:28', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'JFO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.550891', 'null'), +('0e1ea2b6-f4f5-4958-bc48-cfb016a22f58', null, '2020-05-06 18:40:34', 'DAT', 'a913a52e-5e66-4f40-8c64-148f90fa8cd9', null, '2020-05-06 15:40:34', 'SOCR4T3', null, null, null, 'CYP', null, 'DIS', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.557299', '{"catches": [{"nbFish": null, "weight": 100.0, "species": "COD"}], "discardDatetimeUtc": "2020-05-06T11:40:34.449Z"}'), +('3cffa378-0f8c-4540-b849-747621cfcb4a', null, '2020-05-06 18:40:40', 'DAT', '7b487ada-019c-4b62-be32-7d15f7718344', null, '2020-05-06 15:40:40', 'SOCR4T3', null, null, null, 'CYP', '1234567', 'RLC', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.563768', 'null'), +('7bf7401d-cbb1-4e6f-bad8-7e309ee004cf', null, '2020-05-06 18:40:45', 'DAT', 'ced42f65-a1ac-40e1-93c7-851d4933f770', null, '2020-05-06 15:40:45', 'SOCR4T3', null, null, 'GOLF', 'CYP', null, 'RLC', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.570417', 'null'), +('9376ccbd-be2f-4d3d-b4ac-3c559ac9586a', null, '2021-01-31 12:29:02', 'DAT', '8eec0190-c353-4147-8a65-fcc697fbadbc', null, '2021-01-22 09:02:47', 'SOCR4T3', 'OPUF', 'Z.510', 'Dennis', 'BEL', null, 'COE', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.496049', '{"latitudeEntered": 51.333333, "longitudeEntered": 3.2, "faoZoneEntered": "27.4.c", "effortZoneEntered": null, "economicZoneEntered": "BEL", "targetSpeciesOnEntry": "DEMERSAL", "effortZoneEntryDatetimeUtc": "2021-01-22T09:00:00Z", "statisticalRectangleEntered": "31F3"}'); From 0cedb358e04589117616050755bb89115c9190de Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 29 Feb 2024 15:34:56 +0100 Subject: [PATCH 08/82] Update logbook test data --- .../remote_database/V666.5__Reset_test_logbook.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql b/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql index 5bfb46fe74..7921bbba32 100644 --- a/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql +++ b/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql @@ -130,8 +130,8 @@ INSERT INTO logbook_reports ( operation_number, operation_country, operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, trip_number, transmission_format, integration_datetime_utc, value) VALUES ('cc7ee632-e515-460f-a1c1-f82222a6d419', null, '2020-05-06 18:40:51', 'DAT', 'f006a2e5-0fdd-48a0-9a9a-ccae00d052d8', null, '2020-05-06 15:40:51', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'NOT_COX', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.378408', '{"faoZoneExited": null, "latitudeExited": 57.7258, "longitudeExited": 0.5983, "effortZoneExited": null, "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:51.795Z", "statisticalRectangleExited": null}'), ('a3c52754-97e1-4a21-ba2e-d8f16f4544e9', null, '2020-05-06 18:40:57', 'DAT', '9d1ddd34-1394-470e-b8a6-469b86150e1e', null, '2020-05-06 15:40:57', 'SOCR4T3', null, null, null, 'CYP', null, 'COX', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.384086', '{"faoZoneExited": null, "latitudeExited": 46.678, "longitudeExited": -14.616, "effortZoneExited": "A", "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:57.580Z", "statisticalRectangleExited": null}'), -('d5c3b039-aaee-4cca-bcae-637fa8effe14', null, '2020-05-06 18:41:03', 'DAT', '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.38991' , '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z"}'), -('7cfcdde3-286c-4713-8460-2ed82a59be34', null, '2020-05-06 18:41:09', 'DAT', 'fc16ea8a-3148-44b2-977f-de2a2ae550b9', null, '2020-05-06 15:41:09', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.395805', '{"port": "GBPHD", "purpose": "SHE", "tripStartDate": "2020-05-04T19:41:09.200Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:09.200Z"}'), +('d5c3b039-aaee-4cca-bcae-637fa8effe14', null, '2020-05-06 18:41:03', 'DAT', '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.38991' , '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T20:41:03.340Z"}'), +('7cfcdde3-286c-4713-8460-2ed82a59be34', null, '2020-05-06 18:41:09', 'DAT', 'fc16ea8a-3148-44b2-977f-de2a2ae550b9', null, '2020-05-06 15:41:09', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.395805', '{"port": "GBPHD", "purpose": "SHE", "tripStartDate": "2020-05-04T19:41:09.200Z", "predictedArrivalDatetimeUtc": "2020-05-06T20:41:09.200Z"}'), ('4f971076-e6c6-48f6-b87e-deae90fe4705', null, '2020-05-06 18:41:15', 'DAT', 'cc45063f-2d3c-4cda-ac0c-8381e279e150', null, '2020-05-06 15:41:15', 'SOCR4T3', null, null, 'GOLF', 'CYP', null, 'RTP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.401686', '{"port": "ESCAR", "reasonOfReturn": "REF", "returnDatetimeUtc": "2020-05-06T11:41:15.013Z"}'), ('8f06061e-e723-4b89-8577-3801a61582a2', null, '2020-05-06 18:41:20', 'DAT', 'dde5df56-24c2-4a2e-8afb-561f32113256', null, '2020-05-06 15:41:20', 'SOCR4T3', 'IRCS6', 'XR006', null, 'CYP', null, 'RTP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.407777', '{"port": "ESCAR", "gearOnboard": [{"gear": "GN", "mesh": 140.0, "dimensions": 1000.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2020-05-06T11:41:20.712Z"}'), ('8db132d1-68fc-4ae6-b12e-4af594351701', null, '2020-05-06 18:41:26', 'DAT', '83952732-ef89-4168-b2a1-df49d0aa1aff', null, '2020-05-06 15:41:26', 'SOCR4T3', null, null, null, 'CYP', null, 'LAN', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.414081', '{"port": "ESCAR", "sender": null, "catchLanded": [{"nbFish": null, "weight": 100.0, "faoZone": "27.9.b.2", "species": "HAD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.2, "preservationState": "FRO", "statisticalRectangle": null}], "landingDatetimeUtc": "2020-05-05T19:41:26.516Z"}'), @@ -143,7 +143,7 @@ INSERT INTO logbook_reports ( ('5ee8be46-2efe-4a29-b2df-bdf2d3ed66a1', null, '2020-05-06 18:39:40', 'DAT', '7712fe73-cef2-4646-97bb-d634fde00b07', null, '2020-05-06 15:39:40', 'SOCR4T3', null, null, 'GOLF', 'CYP', '1234567', 'DEP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.507524', '{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:40.722Z"}'), ('48794a8f-adfa-43b2-b4c3-2e8d3581bfb4', null, '2020-05-06 18:39:46', 'DAT', '2843bd5b-e4e7-4816-8372-76805201301e', null, '2020-05-06 15:39:46', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'NOT_COE', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.513305', '{"latitudeEntered": 42.794, "longitudeEntered": -13.809, "faoZoneEntered": null, "effortZoneEntered": null, "economicZoneEntered": null, "targetSpeciesOnEntry": null, "effortZoneEntryDatetimeUtc": "2020-05-06T11:39:46.583Z", "statisticalRectangleEntered": null}'), ('196aca16-da66-4077-b340-ecad701be662', null, '2020-05-06 18:39:59', 'DAT', 'b2fca5fb-d1cd-4ec7-8a8c-645cecab6866', null, '2020-05-06 15:39:59', 'SOCR4T3', null, null, null, 'CYP', null, 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.519424', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-06T11:39:59.462Z"}]}'), -('4a4c8d24-f4be-4ccb-8aef-99ab5aae7e02', null, '2020-05-06 18:40:05', 'DAT', '1a87f3de-dea9-4018-8c2e-d6cdfa97318e', null, '2020-05-06 15:40:05', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.525832', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}, {"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 600.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D6"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}]}'), +('4a4c8d24-f4be-4ccb-8aef-99ab5aae7e02', null, '2020-05-06 18:40:05', 'DAT', '1a87f3de-dea9-4018-8c2e-d6cdfa97318e', null, '2020-05-06 15:40:05', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.525832', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T20:40:05.354Z"}, {"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 600.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D6"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}]}'), ('251db84c-1d8b-49be-b426-f70bb2c68a2d', null, '2020-05-06 18:40:11', 'DAT', 'fe7acdb9-ff2e-4cfa-91a9-fd2e06b556e1', null, '2020-05-06 15:40:11', 'SOCR4T3', null, null, null, 'CYP', null, 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.531881', '{"hauls": [{"farDatetimeUtc": "2020-05-06T11:40:11.291Z"}]}'), ('08a125d6-6b6d-4f90-b26a-bf8426673eea', null, '2020-05-06 18:40:17', 'DAT', '74fcd0f7-8117-4791-9aa3-37d5c7dce880', null, '2020-05-06 15:40:17', 'SOCR4T3', null, null, null, 'SVN', null, 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.538061', '{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "species": "BFT"}], "latitude": 39.65, "longitude": 6.83, "farDatetimeUtc": "2020-04-29T12:00:00.000Z"}]}'), ('9e38840b-f05a-49a4-ab34-e41131749fd0', null, '2020-05-06 18:40:22', 'DAT', '1706938b-c3c8-4d34-b32f-54c8d2c0705a', null, '2020-05-06 15:40:22', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'FAR', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.544336', '{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "faoZone": "27.8.e.1", "species": "MZZ", "effortZone": null, "economicZone": null, "statisticalRectangle": null}], "farDatetimeUtc": "2020-05-06T11:40:22.885Z"}]}'), From f516872c83a64658aff0605b25a8324383d85f85 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 29 Feb 2024 15:35:32 +0100 Subject: [PATCH 09/82] Update pno_types test data --- .../V666.32__Insert_test_pno_types.sql | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql b/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql index bdcd6fb37f..44d68e4a92 100644 --- a/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql +++ b/datascience/tests/test_data/remote_database/V666.32__Insert_test_pno_types.sql @@ -11,11 +11,13 @@ INSERT INTO public.pno_types ( ALTER SEQUENCE pno_types_id_seq RESTART WITH 5; INSERT INTO public.pno_type_rules ( - pno_type_id, species, fao_areas, cgpm_areas, gears, flag_states, minimum_quantity_kg) VALUES - ( 1, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 0), - ( 1, '{HKE}', '{37}', '{30.01,30.05,30.06,30.07,30.09,30.10,30.11}', '{}', '{}', 0), - ( 1, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), - ( 2, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 2000), - ( 2, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), - ( 3, '{}', '{}', '{}', '{}', '{GBR,VEN}', 0), - ( 4, '{}', '{}', '{}', '{SB}', '{}', 0); \ No newline at end of file + id, pno_type_id, species, fao_areas, cgpm_areas, gears, flag_states, minimum_quantity_kg) VALUES + ( 1, 1, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 0), + ( 2, 1, '{HKE}', '{37}', '{30.01,30.05,30.06,30.07,30.09,30.10,30.11}', '{}', '{}', 0), + ( 3, 1, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), + ( 4, 2, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 2000), + ( 5, 2, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), + ( 6, 3, '{}', '{}', '{}', '{}', '{GBR,VEN}', 0), + ( 7, 4, '{}', '{}', '{}', '{SB}', '{}', 0); + +ALTER SEQUENCE pno_type_rules_id_seq RESTART WITH 8; \ No newline at end of file From 2477a051b65cd564b70ada55979cc27b89b0d182 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 29 Feb 2024 17:57:21 +0100 Subject: [PATCH 10/82] WIP --- .../src/pipeline/flows/enrich_logbook.py | 261 ++++++++++++++++++ .../monitorfish/pno_species_and_gears.sql | 106 +++++++ .../queries/monitorfish/pno_types.sql | 15 + .../monitorfish/trip_dates_of_pnos.sql | 26 ++ .../V666.5__Reset_test_logbook.sql | 4 +- .../test_flows/test_enrich_logbook.py | 251 +++++++++++++++++ 6 files changed, 662 insertions(+), 1 deletion(-) create mode 100644 datascience/src/pipeline/flows/enrich_logbook.py create mode 100644 datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql create mode 100644 datascience/src/pipeline/queries/monitorfish/pno_types.sql create mode 100644 datascience/src/pipeline/queries/monitorfish/trip_dates_of_pnos.sql create mode 100644 datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py diff --git a/datascience/src/pipeline/flows/enrich_logbook.py b/datascience/src/pipeline/flows/enrich_logbook.py new file mode 100644 index 0000000000..4a111ddd45 --- /dev/null +++ b/datascience/src/pipeline/flows/enrich_logbook.py @@ -0,0 +1,261 @@ +from logging import Logger +from pathlib import Path + +import pandas as pd +import prefect +from prefect import Flow, Parameter, case, task, unmapped +from sqlalchemy import text + +from src.db_config import create_engine +from src.pipeline.generic_tasks import extract +from src.pipeline.helpers.dates import Period +from src.pipeline.processing import prepare_df_for_loading +from src.pipeline.shared_tasks.control_flow import check_flow_not_running +from src.pipeline.shared_tasks.dates import make_periods +from src.pipeline.shared_tasks.segments import extract_all_segments +from src.pipeline.utils import psql_insert_copy + + +@task(checkpoint=False) +def extract_pno_types() -> pd.DataFrame: + return extract( + db_name="monitorfish_remote", query_filepath="monitorfish/pno_types.sql" + ) + + +@task(checkpoint=False) +def reset_pnos(period: Period): + """ + Deletes enriched data from pnos in logbook table in the designated Period. + """ + + logger = prefect.context.get("logger") + e = create_engine("monitorfish_remote") + + logger.info(f"Resetting pnos for period {period.start} - {period.end}.") + + with e.begin() as connection: + connection.execute( + text( + "UPDATE public.logbook_reports p " + "SET " + " enriched = false," + " trip_gears = NULL," + " pno_types = NULL," + " trip_segments = NULL" + "WHERE p.date_time >= :start " + "AND p.date_time <= :end;" + ), + { + "start": period.start, + "end": period.end, + }, + ) + + +def extract_pno_trips_period(period: Period) -> Period: + """ + Extracts the earliest `tripStartDate` and the latest `predictedArrivalDatetimeUtc` + from all PNOs emitted during the given `Period`. + + Args: + period (Period): `Period` of reception of PNOs + + Returns: + Period: `Period` with `start` = `min_trip_start_date` and + `end` = `max_predicted_arrival_datetime_utc` + """ + dates = extract( + db_name="monitorfish_remote", + query_filepath="monitorfish/trip_dates_of_pnos.sql", + params={ + "pno_emission_start_datetime_utc": period.start, + "pno_emission_end_datetime_utc": period.end, + }, + ) + return Period( + start=dates.loc[0, "min_trip_start_date"].to_pydatetime(), + end=dates.loc[0, "max_predicted_arrival_datetime_utc"].to_pydatetime(), + ) + + +def extract_pno_species_and_gears( + pno_emission_period: Period, trips_period: Period +) -> pd.DataFrame: + return extract( + db_name="monitorfish_remote", + query_filepath="monitorfish/pno_species_and_gears.sql", + params={ + "min_pno_date": pno_emission_period.start, + "max_pno_date": pno_emission_period.end, + "min_trip_date": trips_period.start, + "max_trip_date": trips_period.end, + }, + ) + + +def compute_pno_segments( + pno_species_and_gears: pd.DataFrame, segments: pd.DataFrame +) -> pd.DataFrame: + pass + + +def compute_pno_types( + pno_species_and_gears: pd.DataFrame, pno_types: pd.DataFrame +) -> pd.DataFrame: + breakpoint() + + +def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logger): + e = create_engine("monitorfish_remote") + + with e.begin() as connection: + logger.info("Creating temporary table") + connection.execute( + text( + "CREATE TEMP TABLE tmp_enriched_pnos(" + " id INTEGER PRIMARY KEY," + " is_at_port BOOLEAN," + " meters_from_previous_position REAL," + " time_since_previous_position INTERVAL," + " average_speed REAL," + " is_fishing BOOLEAN," + " time_emitting_at_sea INTERVAL" + ")" + "ON COMMIT DROP;" + ) + ) + + enriched_pnos = prepare_df_for_loading( + enriched_pnos, + logger, + ) + + columns_to_load = [ + "id", + "is_at_port", + "meters_from_previous_position", + "time_since_previous_position", + "average_speed", + "is_fishing", + "time_emitting_at_sea", + ] + + logger.info("Loading to temporary table") + + enriched_pnos[columns_to_load].to_sql( + "tmp_enriched_pnos", + connection, + if_exists="append", + index=False, + method=psql_insert_copy, + ) + + logger.info("Updating pnos from temporary table") + + connection.execute( + text( + "UPDATE public.logbook p " + "SET " + " is_at_port = ep.is_at_port, " + " meters_from_previous_position = COALESCE( " + " ep.meters_from_previous_position, " + " p.meters_from_previous_position " + " ), " + " time_since_previous_position = COALESCE( " + " ep.time_since_previous_position, " + " p.time_since_previous_position " + " ), " + " average_speed = COALESCE( " + " ep.average_speed, " + " p.average_speed " + " ), " + " is_fishing = COALESCE( " + " ep.is_fishing, " + " p.is_fishing " + " )," + " time_emitting_at_sea = COALESCE( " + " ep.time_emitting_at_sea, " + " p.time_emitting_at_sea " + " )" + "FROM tmp_enriched_pnos ep " + "WHERE p.id = ep.id " + "AND p.date_time >= :start " + "AND p.date_time <= :end;" + ), + { + "start": period.start, + "end": period.end, + }, + ) + + +@task(checkpoint=False) +def extract_enrich_load_logbook( + period: Period, segments: pd.DataFrame, pno_types: pd.DataFrame +): + """Extract pnos for the given `Period`, enrich and update the `logbook` table. + + This is all done in one `Task` in order to avoid having tasks returning anything. + Indeed Prefect stores all task results in memory until the flow run is done + running, which in this case must be avoided in order to benefit from the chunked + processing logic in terms of memory consumption. + """ + + logger = prefect.context.get("logger") + logger.info(f"Processing pnos for period {period.start} - {period.end}.") + + trips_period = extract_pno_trips_period() + + logger.info("Extracting PNO...") + pnos_species_and_gears = extract_pno_species_and_gears( + pno_emission_period=period, trips_period=trips_period + ) + logger.info( + f"Extracted {len(pnos_species_and_gears)} PNO species from {pnos_species_and_gears.id.nunique()} PNOs." + ) + + logger.info("Computing PNO segments...") + pnos = compute_pno_segments( + pno_species_and_gears=pnos_species_and_gears, segments=segments + ) + + logger.info("Loading") + load_enriched_pnos(pnos, period, logger) + + +with Flow("Enrich Logbook") as flow: + flow_not_running = check_flow_not_running() + with case(flow_not_running, True): + start_hours_ago = Parameter("start_hours_ago") + end_hours_ago = Parameter("end_hours_ago") + minutes_per_chunk = Parameter("minutes_per_chunk") + recompute_all = Parameter("recompute_all") + + periods = make_periods( + start_hours_ago, + end_hours_ago, + minutes_per_chunk, + 0, + ) + + segments = extract_all_segments() + pno_types = extract_pno_types() + + with case(recompute_all, True): + reset = reset_pnos.map(periods) + extract_enrich_load_logbook.map( + periods, + segments=unmapped(segments), + pno_types=unmapped(pno_types), + upstream_tasks=[reset], + ) + + with case(recompute_all, False): + extract_enrich_load_logbook.map( + periods, + segments=unmapped(segments), + pno_types=unmapped(pno_types), + ) + +flow.file_name = Path(__file__).name diff --git a/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql b/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql new file mode 100644 index 0000000000..aa9f0f1ae3 --- /dev/null +++ b/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql @@ -0,0 +1,106 @@ +WITH deleted_corrected_or_rejected_messages AS ( + SELECT referenced_report_id + FROM logbook_reports + WHERE + operation_datetime_utc >= :min_pno_date - INTERVAL '1 day' + AND operation_datetime_utc <= :max_pno_date + INTERVAL '1 week' + AND + ( + operation_type IN ('COR', 'DEL') + OR ( + operation_type = 'RET' + AND value->>'returnStatus' = '002' + ) + ) +), + +pno_species AS ( + SELECT + id, + cfr, + flag_state, + trip_number, + report_datetime_utc, + (r.value->>'tripStartDate')::TIMESTAMPTZ AS trip_start_date, + (r.value->>'predictedArrivalDatetimeUtc')::TIMESTAMPTZ AS predicted_arrival_datetime_utc, + catch->>'species' AS species, + catch->>'faoZone' AS fao_area, + (catch->>'weight')::DOUBLE PRECISION AS weight + FROM logbook_reports r + LEFT JOIN jsonb_array_elements(value->'catchOnboard') catch ON true + WHERE + operation_datetime_utc >= :min_pno_date + AND operation_datetime_utc < :max_pno_date + AND log_type = 'PNO' + AND NOT enriched + AND report_id NOT IN (SELECT referenced_report_id FROM deleted_corrected_or_rejected_messages) +), + +pno_trips AS ( + SELECT DISTINCT + id, + cfr, + report_datetime_utc, + trip_start_date, + predicted_arrival_datetime_utc + FROM pno_species +), + +far_gears AS ( + SELECT + t.id, + jsonb_agg( + DISTINCT jsonb_build_object( + 'gear', haul->>'gear', + 'mesh', (haul->>'mesh')::DOUBLE PRECISION, + 'dimensions', haul->>'dimensions' + ) + ) AS far_gears + FROM pno_trips t + LEFT JOIN logbook_reports far + ON + far.operation_datetime_utc >= :min_trip_date + AND far.operation_datetime_utc < :max_trip_date + AND far.log_type = 'FAR' + AND far.cfr = t.cfr + AND far.report_id NOT IN (SELECT referenced_report_id FROM deleted_corrected_or_rejected_messages) + JOIN jsonb_array_elements(far.value->'hauls') haul + ON + (haul->>'farDatetimeUtc')::TIMESTAMPTZ >= t.trip_start_date + AND (haul->>'farDatetimeUtc')::TIMESTAMPTZ <= GREATEST(t.predicted_arrival_datetime_utc, t.report_datetime_utc AT TIME ZONE 'UTC') + AND haul->>'gear' IS NOT NULL + GROUP BY t.id +), + +dep_gears AS ( + SELECT DISTINCT ON (t.id) + t.id, + dep.value->'gearOnboard' AS dep_gears + FROM pno_trips t + JOIN logbook_reports dep + ON + dep.operation_datetime_utc >= '2024-01-07' + AND dep.operation_datetime_utc < '2024-03-01' + AND dep.log_type = 'DEP' + AND dep.cfr = t.cfr + AND dep.report_id NOT IN (SELECT referenced_report_id FROM deleted_corrected_or_rejected_messages) + -- sometimes the tripStartDate of PNO messages is rounded a few hours after the actual departure. Adding a 24h buffer is a safety measure to find the DEP in these cases. + -- Taking the most recent one (with DISTINCT ON / ORDER BY) avoids taking a dep from the previous trip. + AND (value->>'departureDatetimeUtc')::TIMESTAMPTZ >= t.trip_start_date - INTERVAL '24 hours' + AND (value->>'departureDatetimeUtc')::TIMESTAMPTZ <= GREATEST(t.predicted_arrival_datetime_utc, t.report_datetime_utc AT TIME ZONE 'UTC') + ORDER BY id, (value->>'departureDatetimeUtc')::TIMESTAMPTZ DESC +) + +SELECT + s.id AS logbook_reports_pno_id, + EXTRACT('YEAR' FROM predicted_arrival_datetime_utc)::INTEGER AS year, + s.species, + COALESCE(fg.far_gears, dg.dep_gears) AS trip_gears, + s.fao_area, + s.weight, + s.flag_state +FROM pno_species s +LEFT JOIN far_gears fg +ON s.id = fg.id +LEFT JOIN dep_gears dg +ON s.id = dg.id \ No newline at end of file diff --git a/datascience/src/pipeline/queries/monitorfish/pno_types.sql b/datascience/src/pipeline/queries/monitorfish/pno_types.sql new file mode 100644 index 0000000000..b2818792b5 --- /dev/null +++ b/datascience/src/pipeline/queries/monitorfish/pno_types.sql @@ -0,0 +1,15 @@ +SELECT + t.id AS pno_type_id, + t.name AS pno_type_name, + t.minimum_notification_period, + t.has_designated_ports, + r.id AS pno_type_rule_id, + r.species, + r.fao_areas, + r.gears, + r.flag_states, + r.minimum_quantity_kg +FROM pno_types t +JOIN pno_type_rules r +ON t.id = r.pno_type_id +ORDER BY t.id, r.id \ No newline at end of file diff --git a/datascience/src/pipeline/queries/monitorfish/trip_dates_of_pnos.sql b/datascience/src/pipeline/queries/monitorfish/trip_dates_of_pnos.sql new file mode 100644 index 0000000000..8a4268a4a9 --- /dev/null +++ b/datascience/src/pipeline/queries/monitorfish/trip_dates_of_pnos.sql @@ -0,0 +1,26 @@ +WITH deleted_corrected_or_rejected_messages AS ( + SELECT referenced_report_id + FROM logbook_reports + WHERE + operation_datetime_utc >= :pno_emission_start_datetime_utc - INTERVAL '1 day' + AND operation_datetime_utc <= :pno_emission_end_datetime_utc + INTERVAL '1 week' + AND + ( + operation_type IN ('COR', 'DEL') + OR ( + operation_type = 'RET' + AND value->>'returnStatus' = '002' + ) + ) +) + +SELECT + MIN((r.value->>'tripStartDate')::TIMESTAMPTZ) AS min_trip_start_date, + MAX((r.value->>'predictedArrivalDatetimeUtc')::TIMESTAMPTZ) AS max_predicted_arrival_datetime_utc +FROM logbook_reports r +WHERE + --operation_datetime_utc AT TIME ZONE 'UTC' >= :pno_emission_start_datetime_utc + operation_datetime_utc <= :pno_emission_end_datetime_utc + INTERVAL '2 hours 41 minutes' + AND log_type = 'PNO' + AND NOT enriched + AND report_id NOT IN (SELECT referenced_report_id FROM deleted_corrected_or_rejected_messages) diff --git a/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql b/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql index 7921bbba32..0add22e0c7 100644 --- a/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql +++ b/datascience/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql @@ -1,6 +1,8 @@ DELETE FROM logbook_reports; DELETE FROM logbook_raw_messages; +ALTER SEQUENCE logbook_report_id_seq RESTART WITH 1; + INSERT INTO logbook_raw_messages (operation_number, xml_message) VALUES ('1', 'Message ERS xml'), ('2', 'Message ERS xml'), @@ -130,7 +132,7 @@ INSERT INTO logbook_reports ( operation_number, operation_country, operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, trip_number, transmission_format, integration_datetime_utc, value) VALUES ('cc7ee632-e515-460f-a1c1-f82222a6d419', null, '2020-05-06 18:40:51', 'DAT', 'f006a2e5-0fdd-48a0-9a9a-ccae00d052d8', null, '2020-05-06 15:40:51', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', '1234567', 'NOT_COX', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.378408', '{"faoZoneExited": null, "latitudeExited": 57.7258, "longitudeExited": 0.5983, "effortZoneExited": null, "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:51.795Z", "statisticalRectangleExited": null}'), ('a3c52754-97e1-4a21-ba2e-d8f16f4544e9', null, '2020-05-06 18:40:57', 'DAT', '9d1ddd34-1394-470e-b8a6-469b86150e1e', null, '2020-05-06 15:40:57', 'SOCR4T3', null, null, null, 'CYP', null, 'COX', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.384086', '{"faoZoneExited": null, "latitudeExited": 46.678, "longitudeExited": -14.616, "effortZoneExited": "A", "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:57.580Z", "statisticalRectangleExited": null}'), -('d5c3b039-aaee-4cca-bcae-637fa8effe14', null, '2020-05-06 18:41:03', 'DAT', '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.38991' , '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T20:41:03.340Z"}'), +('d5c3b039-aaee-4cca-bcae-637fa8effe14', null, '2020-05-06 18:41:03', 'DAT', '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.38991' , '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL", "faoZone": "27.7.a"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T20:41:03.340Z"}'), ('7cfcdde3-286c-4713-8460-2ed82a59be34', null, '2020-05-06 18:41:09', 'DAT', 'fc16ea8a-3148-44b2-977f-de2a2ae550b9', null, '2020-05-06 15:41:09', 'SOCR4T3', null, null, null, 'CYP', null, 'PNO', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.395805', '{"port": "GBPHD", "purpose": "SHE", "tripStartDate": "2020-05-04T19:41:09.200Z", "predictedArrivalDatetimeUtc": "2020-05-06T20:41:09.200Z"}'), ('4f971076-e6c6-48f6-b87e-deae90fe4705', null, '2020-05-06 18:41:15', 'DAT', 'cc45063f-2d3c-4cda-ac0c-8381e279e150', null, '2020-05-06 15:41:15', 'SOCR4T3', null, null, 'GOLF', 'CYP', null, 'RTP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.401686', '{"port": "ESCAR", "reasonOfReturn": "REF", "returnDatetimeUtc": "2020-05-06T11:41:15.013Z"}'), ('8f06061e-e723-4b89-8577-3801a61582a2', null, '2020-05-06 18:41:20', 'DAT', 'dde5df56-24c2-4a2e-8afb-561f32113256', null, '2020-05-06 15:41:20', 'SOCR4T3', 'IRCS6', 'XR006', null, 'CYP', null, 'RTP', 'SRC-TRP-TTT20200506194051795', 'FLUX' , '2022-03-31 09:21:19.407777', '{"port": "ESCAR", "gearOnboard": [{"gear": "GN", "mesh": 140.0, "dimensions": 1000.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2020-05-06T11:41:20.712Z"}'), diff --git a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py new file mode 100644 index 0000000000..993e76cbf9 --- /dev/null +++ b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py @@ -0,0 +1,251 @@ +from datetime import datetime + +import pandas as pd +import pytest +import pytz + +from src.pipeline.flows.enrich_logbook import ( + compute_pno_types, + extract_pno_species_and_gears, + extract_pno_trips_period, + extract_pno_types, + flow, +) +from src.pipeline.helpers.dates import Period +from tests.mocks import mock_check_flow_not_running + +flow.replace(flow.get_tasks("check_flow_not_running")[0], mock_check_flow_not_running) + + +@pytest.fixture +def expected_pno_types() -> pd.DataFrame: + return pd.DataFrame( + { + "pno_type_id": [1, 1, 1, 2, 2, 3, 4], + "pno_type_name": [ + "Préavis type 1", + "Préavis type 1", + "Préavis type 1", + "Préavis type 2", + "Préavis type 2", + "Préavis par pavillon", + "Préavis par engin", + ], + "minimum_notification_period": [4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0], + "has_designated_ports": [True, True, True, True, True, True, True], + "pno_type_rule_id": [1, 2, 3, 4, 5, 6, 7], + "species": [ + ["HKE", "BSS", "COD", "ANF", "SOL"], + ["HKE"], + ["HER", "MAC", "HOM", "WHB"], + ["HKE", "BSS", "COD", "ANF", "SOL"], + ["HER", "MAC", "HOM", "WHB"], + [], + [], + ], + "fao_areas": [ + [ + "27.3.a", + "27.4", + "27.6", + "27.7", + "27.8.a", + "27.8.b", + "27.8.c", + "27.8.d", + "27.9.a", + ], + ["37"], + ["27", "34.1.2", "34.2"], + [ + "27.3.a", + "27.4", + "27.6", + "27.7", + "27.8.a", + "27.8.b", + "27.8.c", + "27.8.d", + "27.9.a", + ], + ["27", "34.1.2", "34.2"], + [], + [], + ], + "gears": [[], [], [], [], [], [], ["SB"]], + "flag_states": [[], [], [], [], [], ["GBR", "VEN"], []], + "minimum_quantity_kg": [0.0, 0.0, 10000.0, 2000.0, 10000.0, 0.0, 0.0], + } + ) + + +@pytest.fixture +def sample_pno_species_and_gears() -> pd.DataFrame: + return pd.DataFrame( + { + "logbook_reports_pno_id": [1, 2, 3, 4, 4, 4, 5, 5, 5, 6, 7, 8], + "year": [ + 2021, + 2022, + 2023, + 2023, + 2023, + 2023, + 2023, + 2023, + 2023, + 2023, + 2023, + 2023, + ], + "species": [ + "HKE", + "HKE", + "HKE", + "BSS", + "COD", + "COD", + "MAC", + "HOM", + "HER", + "HKE", + None, + None, + ], + "trip_gears": [ + [ + {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, + {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, + ], + [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], + None, + [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], + [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], + [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], + [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], + [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], + [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], + [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], + [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], + [{"gear": "SB", "mesh": 20, "dimensions": "4.5"}], + ], + "fao_area": [ + "27.9.a", + "27.9.a", + "37.1.3", + "27.7.d", + "27.8.c", + "27.10.c", + "27.7.d", + "27.8.a", + "34.1.2", + "27.2.a", + None, + None, + ], + "weight": [ + 1500.0, + 2500.0, + 2500.0, + 800.0, + 800.0, + 800.0, + 5000.0, + 5000.0, + 5000.0, + 3500.0, + None, + None, + ], + "flag_state": [ + "FRA", + "FRA", + "GBR", + "FRA", + "FRA", + "FRA", + "FRA", + "FRA", + "FRA", + "FRA", + "FRA", + "FRA", + ], + } + ) + + +@pytest.fixture +def expected_pno_species_and_gears() -> pd.DataFrame: + return pd.DataFrame( + { + "logbook_reports_pno_id": [12, 13], + "year": [2020, 2020], + "species": ["GHL", None], + "trip_gears": [ + [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], + [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], + ], + "fao_area": ["27.7.a", None], + "weight": [1500.0, None], + "flag_state": ["CYP", "CYP"], + } + ) + + +def test_extract_pno_types(reset_test_data, expected_pno_types): + pno_types = extract_pno_types.run() + pd.testing.assert_frame_equal(pno_types, expected_pno_types) + + +def test_extract_pno_trips_period(reset_test_data): + pno_period = Period( + start=datetime(2020, 5, 6, 18, 30, 0), end=datetime(2020, 5, 6, 18, 50, 0) + ) + expected_trips_period = Period( + start=pytz.UTC.localize(datetime(2020, 5, 4, 19, 41, 3, 340000)), + end=pytz.UTC.localize(datetime(2020, 5, 6, 20, 41, 9, 200000)), + ) + trips_period = extract_pno_trips_period(period=pno_period) + assert trips_period == expected_trips_period + + +def test_extract_pno_species_and_gears(reset_test_data, expected_pno_species_and_gears): + pno_species_and_gears = extract_pno_species_and_gears( + pno_emission_period=Period( + start=datetime(2020, 5, 6, 18, 30, 0), end=datetime(2020, 5, 6, 18, 50, 0) + ), + trips_period=Period( + start=pytz.UTC.localize(datetime(2020, 5, 4, 19, 41, 3, 340000)), + end=pytz.UTC.localize(datetime(2020, 5, 6, 20, 41, 9, 200000)), + ), + ) + + pd.testing.assert_frame_equal( + pno_species_and_gears.sort_values("logbook_reports_pno_id").reset_index( + drop=True + ), + expected_pno_species_and_gears, + ) + + +def test_compute_pno_types(expected_pno_types, sample_pno_species_and_gears): + compute_pno_types(sample_pno_species_and_gears, expected_pno_types) + + +def test_load_then_reset_logbook(reset_test_data): + # pnos = read_query("SELECT * FROM logbook_reports WHERE log_type = 'PNO'", db="monitorfish_remote") + # breakpoint() + pass + + +def test_extract_enrich_load(reset_test_data): + pass + + +def test_flow_does_not_recompute_all_when_not_asked_to(reset_test_data): + pass + + +def test_flow_recomputes_all_when_asked_to(reset_test_data): + pass From c65daf6dfe2771007bdba19dc12d74dfed2dbced Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 1 Mar 2024 14:24:25 +0100 Subject: [PATCH 11/82] Mose segments test data to fixture --- .../queries/monitorfish/fleet_segments.sql | 3 +- .../test_shared_tasks/test_segments.py | 58 ++++++++++--------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/datascience/src/pipeline/queries/monitorfish/fleet_segments.sql b/datascience/src/pipeline/queries/monitorfish/fleet_segments.sql index 37d3fc2e99..b2d0abc6ff 100644 --- a/datascience/src/pipeline/queries/monitorfish/fleet_segments.sql +++ b/datascience/src/pipeline/queries/monitorfish/fleet_segments.sql @@ -6,4 +6,5 @@ SELECT fao_areas, target_species || bycatch_species as species, impact_risk_factor -FROM public.fleet_segments \ No newline at end of file +FROM public.fleet_segments +ORDER BY year, segment \ No newline at end of file diff --git a/datascience/tests/test_pipeline/test_shared_tasks/test_segments.py b/datascience/tests/test_pipeline/test_shared_tasks/test_segments.py index 1abf5837f1..a8c782f7dc 100644 --- a/datascience/tests/test_pipeline/test_shared_tasks/test_segments.py +++ b/datascience/tests/test_pipeline/test_shared_tasks/test_segments.py @@ -1,6 +1,7 @@ from datetime import datetime import pandas as pd +import pytest from src.pipeline.shared_tasks.segments import ( extract_all_segments, @@ -9,32 +10,8 @@ ) -def test_extract_segments_of_year(reset_test_data): - current_year = datetime.utcnow().year - segments = extract_segments_of_year.run(current_year) - - expected_segments = pd.DataFrame( - { - "segment": ["SWW01/02/03", "SWW04"], - "segment_name": ["Bottom trawls", "Midwater trawls"], - "gears": [ - ["OTB", "OTT", "PTB", "OT", "PT", "TBN", "TBS", "TX", "TB"], - ["OTM", "PTM"], - ], - "fao_areas": [["27.8.c", "27.8", "27.9"], ["27.8.c", "27.8"]], - "species": [["ANF", "HKE", "LEZ", "MNZ", "NEP", "SOL"], ["HKE"]], - "impact_risk_factor": [3.0, 2.1], - } - ) - - pd.testing.assert_frame_equal( - segments.sort_values("segment").reset_index(drop=True), - expected_segments.sort_values("segment").reset_index(drop=True), - ) - - -def test_extract_all_segments(reset_test_data): - segments = extract_all_segments.run() +@pytest.fixture +def expected_all_segments() -> pd.DataFrame: current_year = datetime.utcnow().year expected_segments = pd.DataFrame( { @@ -67,13 +44,38 @@ def test_extract_all_segments(reset_test_data): "impact_risk_factor": [3.0, 2.1, 3.0, 2.1], } ) + return expected_segments + + +def test_extract_segments_of_year(reset_test_data): + current_year = datetime.utcnow().year + segments = extract_segments_of_year.run(current_year) + + expected_segments = pd.DataFrame( + { + "segment": ["SWW01/02/03", "SWW04"], + "segment_name": ["Bottom trawls", "Midwater trawls"], + "gears": [ + ["OTB", "OTT", "PTB", "OT", "PT", "TBN", "TBS", "TX", "TB"], + ["OTM", "PTM"], + ], + "fao_areas": [["27.8.c", "27.8", "27.9"], ["27.8.c", "27.8"]], + "species": [["ANF", "HKE", "LEZ", "MNZ", "NEP", "SOL"], ["HKE"]], + "impact_risk_factor": [3.0, 2.1], + } + ) pd.testing.assert_frame_equal( - segments.sort_values(["year", "segment"]).reset_index(drop=True), - expected_segments.sort_values(["year", "segment"]).reset_index(drop=True), + segments.sort_values("segment").reset_index(drop=True), + expected_segments.sort_values("segment").reset_index(drop=True), ) +def test_extract_all_segments(reset_test_data, expected_all_segments): + segments = extract_all_segments.run() + pd.testing.assert_frame_equal(segments, expected_all_segments) + + def test_unnest_segments(): segments_definitions = [ [ From 5df059b4c136c3b63ba977e932d54b1b4d1ec8c4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 1 Mar 2024 14:25:37 +0100 Subject: [PATCH 12/82] Share segments fixtures --- datascience/tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/datascience/tests/conftest.py b/datascience/tests/conftest.py index d4884763e6..019fcd5477 100644 --- a/datascience/tests/conftest.py +++ b/datascience/tests/conftest.py @@ -244,3 +244,9 @@ def reset_test_data(create_tables): for s in test_data_scripts: print(f"{s.major}.{s.minor}.{s.patch}: {s.path.name}") connection.execute(text(s.script)) + + +############################ Share fixtures between modules ############################ +pytest_plugins = [ + "tests.test_pipeline.test_shared_tasks.test_segments", +] From d872e2e2f1939127fb00de01fb9b452db2db703b Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 1 Mar 2024 14:27:34 +0100 Subject: [PATCH 13/82] Remove unused code --- datascience/tests/conftest.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/datascience/tests/conftest.py b/datascience/tests/conftest.py index 019fcd5477..fc84c3e92f 100644 --- a/datascience/tests/conftest.py +++ b/datascience/tests/conftest.py @@ -202,39 +202,6 @@ def create_tables(set_environment_variables, start_remote_database_container): ) -# @pytest.fixture(scope="session") -# def start_remote_database_container(create_docker_client): -# client = create_docker_client -# print("Starting database container") -# remote_database_container = client.containers.run( -# "timescale/timescaledb-postgis:1.7.4-pg11", -# environment={ -# "POSTGRES_PASSWORD": os.environ["MONITORFISH_REMOTE_DB_PWD"], -# "POSTGRES_USER": os.environ["MONITORFISH_REMOTE_DB_USER"], -# "POSTGRES_DB": os.environ["MONITORFISH_REMOTE_DB_NAME"], -# }, -# ports={"5432/tcp": 5434}, -# detach=True, -# ) -# sleep(3) -# yield -# print("Stopping database container") -# remote_database_container.stop() -# remote_database_container.remove(v=True) - - -# @pytest.fixture(scope="session") -# def create_tables(start_remote_database_container): -# e = create_engine("monitorfish_remote") -# migrations = get_migrations_in_folders(migrations_folders) -# print("Creating tables") -# with e.connect() as connection: -# for m in migrations: -# print(f"{m.major}.{m.minor}.{m.patch}: {m.path.name}") -# connection.execute(text("COMMIT")) -# connection.execute(text(m.script)) - - @pytest.fixture() def reset_test_data(create_tables): e = create_engine("monitorfish_remote") From a604956082309989f337f998e3e1f5645f92bb43 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 1 Mar 2024 18:00:35 +0100 Subject: [PATCH 14/82] Update logbook_reports migration --- .../migration/internal/V0.245__Update_logbook_reports_table.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql index bc3d1fc872..c499e85db7 100644 --- a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql +++ b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql @@ -2,4 +2,4 @@ ALTER TABLE public.logbook_reports ADD COLUMN enriched BOOLEAN NOT NULL DEFAULT false, ADD COLUMN trip_gears jsonb, ADD COLUMN pno_types jsonb, - ADD COLUMN trip_segments VARCHAR[]; + ADD COLUMN trip_segments jsonb; From cf90d0f40acd777dd3b4874f0891178ae441fd3e Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Mar 2024 09:12:04 +0100 Subject: [PATCH 15/82] Add enriched_logbook flow --- .../src/pipeline/flows/enrich_logbook.py | 296 ++++++++++-- .../test_flows/test_enrich_logbook.py | 449 +++++++++++++++++- 2 files changed, 684 insertions(+), 61 deletions(-) diff --git a/datascience/src/pipeline/flows/enrich_logbook.py b/datascience/src/pipeline/flows/enrich_logbook.py index 4a111ddd45..78847edbbb 100644 --- a/datascience/src/pipeline/flows/enrich_logbook.py +++ b/datascience/src/pipeline/flows/enrich_logbook.py @@ -1,6 +1,7 @@ from logging import Logger from pathlib import Path +import duckdb import pandas as pd import prefect from prefect import Flow, Parameter, case, task, unmapped @@ -42,9 +43,10 @@ def reset_pnos(period: Period): " enriched = false," " trip_gears = NULL," " pno_types = NULL," - " trip_segments = NULL" - "WHERE p.date_time >= :start " - "AND p.date_time <= :end;" + " trip_segments = NULL " + "WHERE p.operation_datetime_utc >= :start " + "AND p.operation_datetime_utc <= :end " + "AND log_type = 'PNO';" ), { "start": period.start, @@ -97,13 +99,221 @@ def extract_pno_species_and_gears( def compute_pno_segments( pno_species_and_gears: pd.DataFrame, segments: pd.DataFrame ) -> pd.DataFrame: - pass + """ + Computes the segments of the input PNO species and gears. + + Args: + pno_species_and_gears (pd.DataFrame): DataFrame of PNO species. 1 line = catch. + Must have columns : + + - logbook_reports_pno_id `int` `1` + - trip_gears `List[dict]` + `[{"gear": "xxx", "mesh": yyy, "dimensions": "zzz}, {...}]` + - species `str` `'COD'` + - fao_area `str` `'27.7.d'` + - year `int` `2022` + + segments (pd.DataFrame): DataFrame of segments definitions. 1 line = 1 segment. + Must have columns : + + - year `int` `2022` + - segment `str` `SWW1` + - segment_name `str` `Nom du segment` + - gears `List[str]` `["OTB", ...]` + - fao_areas `List[str]` `["27.8", ...]` + - species `List[str]` `["COD", ...]` + + Returns: + pd.DataFrame: DataFrame of PNOs with attributed PNO types. 1 line = 1 PNO. + Has columns: + + - logbook_reports_pno_id `int` `1` + - trip_gears `List[dict]` + `[{"gear": "xxx", "mesh": yyy, "dimensions": "zzz}, {...}]` + - pno_types `List[dict]` + ```[ + { + "pno_type_name": "Type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True + }, + {...} + ]``` + """ + + db = duckdb.connect() + + res = db.sql( + """ + WITH trip_gear_codes AS ( + SELECT logbook_reports_pno_id, ARRAY_AGG(DISTINCT gear->>'gear') AS trip_gear_codes + FROM pno_species_and_gears, unnest(trip_gears) AS t(gear) + GROUP BY logbook_reports_pno_id + ), + + trip_ids AS ( + SELECT DISTINCT logbook_reports_pno_id + FROM pno_species_and_gears + ), + + pnos_with_segments AS ( + SELECT + sg.logbook_reports_pno_id, + LIST_SORT(ARRAY_AGG(DISTINCT { + 'segment': s.segment, + 'segment_name': s.segment_name + })) AS trip_segments + FROM pno_species_and_gears sg + LEFT JOIN trip_gear_codes tgc + ON tgc.logbook_reports_pno_id = sg.logbook_reports_pno_id + JOIN segments s + ON + (sg.species = ANY(s.species) OR s.species = '[]'::VARCHAR[]) AND + (list_has_any(tgc.trip_gear_codes, s.gears) OR s.gears = '[]'::VARCHAR[]) AND + (length(filter(s.fao_areas, a -> sg.fao_area LIKE a || '%')) > 0 OR s.fao_areas = '[]'::VARCHAR[]) AND + s.year = sg.year + GROUP BY 1 + ) + + SELECT t.logbook_reports_pno_id, s.trip_segments + FROM trip_ids t + LEFT JOIN pnos_with_segments s + ON t.logbook_reports_pno_id = s.logbook_reports_pno_id + ORDER BY 1 + """ + ).to_df() + + return res def compute_pno_types( pno_species_and_gears: pd.DataFrame, pno_types: pd.DataFrame ) -> pd.DataFrame: - breakpoint() + """ + Computes the PNO types of the input PNO species and gears. + + Args: + pno_species_and_gears (pd.DataFrame): DataFrame of PNO species. 1 line = catch. + Must have columns : + + - logbook_reports_pno_id `int` `1` + - trip_gears `List[dict]` + `[{"gear": "xxx", "mesh": yyy, "dimensions": "zzz}, {...}]` + - species `str` `'COD'` + - fao_area `str` `'27.7.d'` + - flag_state `str` `'FRA'` + - weight `float` `150.5` + + pno_types (pd.DataFrame): DataFrame of pno_types definitions. 1 line = 1 rule. + Must have columns : + + - pno_type_id `int` `1` + - pno_type_name `str` `"Ports désignés thon rouge"` + - minimum_notification_period `float` `4.0` + - has_designated_ports `bool` `True` + - pno_type_rule_id `int` `1` + - species `List[str]` `["COD", ...]` + - gears `List[str]` `["OTB", ...]` + - fao_areas `List[str]` `["27.8", ...]` + - flag_states `List[str]` `["GBR", ...]` + - minimum_quantity_kg `float` `2500.0` + + Returns: + pd.DataFrame: DataFrame of PNOs with attributed PNO types. 1 line = 1 PNO. + Has columns: + + - logbook_reports_pno_id `int` `1` + - trip_gears `List[dict]` + `[{"gear": "xxx", "mesh": yyy, "dimensions": "zzz}, {...}]` + - pno_types `List[dict]` + ```[ + { + "pno_type_name": "Type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True + }, + {...} + ]``` + """ + db = duckdb.connect() + + res = db.sql( + """ + WITH trip_gear_codes AS ( + SELECT logbook_reports_pno_id, ARRAY_AGG(DISTINCT gear->>'gear') AS trip_gear_codes + FROM pno_species_and_gears, unnest(trip_gears) AS t(gear) + GROUP BY logbook_reports_pno_id + ), + + pnos_pno_types_tmp AS ( + SELECT + sg.logbook_reports_pno_id, + t.pno_type_name, + t.minimum_notification_period, + t.has_designated_ports, + t.minimum_quantity_kg, + SUM(COALESCE(weight, 0)) OVER (PARTITION BY sg.logbook_reports_pno_id, pno_type_rule_id) AS pno_quantity_kg + FROM pno_species_and_gears sg + LEFT JOIN trip_gear_codes tgc + ON tgc.logbook_reports_pno_id = sg.logbook_reports_pno_id + JOIN pno_types t + ON + (sg.species = ANY(t.species) OR t.species = '[]'::VARCHAR[]) AND + (list_has_any(tgc.trip_gear_codes, t.gears) OR t.gears = '[]'::VARCHAR[]) AND + (length(filter(t.fao_areas, a -> sg.fao_area LIKE a || '%')) > 0 OR t.fao_areas = '[]'::VARCHAR[]) AND + (sg.flag_state = ANY(t.flag_states) OR t.flag_states = '[]'::VARCHAR[]) + ), + + pnos_pno_types AS ( + SELECT + logbook_reports_pno_id, + LIST_SORT(ARRAY_AGG(DISTINCT { + 'pno_type_name': pno_type_name, + 'minimum_notification_period': minimum_notification_period, + 'has_designated_ports': has_designated_ports + })) AS pno_types + FROM pnos_pno_types_tmp + WHERE pno_quantity_kg >= minimum_quantity_kg + GROUP BY logbook_reports_pno_id + ), + + pnos_trip_gears AS ( + SELECT DISTINCT ON (logbook_reports_pno_id) + logbook_reports_pno_id, + LIST_SORT(trip_gears) AS trip_gears + FROM pno_species_and_gears + ORDER BY logbook_reports_pno_id + ) + + SELECT + g.logbook_reports_pno_id, + g.trip_gears, + t.pno_types + FROM pnos_trip_gears g + LEFT JOIN pnos_pno_types t + ON g.logbook_reports_pno_id = t.logbook_reports_pno_id + ORDER BY g.logbook_reports_pno_id + """ + ).to_df() + + return res + + +def merge_segments_and_types( + pnos_with_types: pd.DataFrame, pnos_with_segments: pd.DataFrame +) -> pd.DataFrame: + """ + Merges the input DataFrames on `logbook_reports_pno_id` + + Args: + pnos_with_types (pd.DataFrame): DataFrame of PNOs with their types + pnos_with_segments (pd.DataFrame): DataFrame of PNOs with their segments + + Returns: + pd.DataFrame: DataFrame of PNOs with their types and segments + """ + + return pd.merge(pnos_with_types, pnos_with_segments, on="logbook_reports_pno_id") def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logger): @@ -114,13 +324,11 @@ def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logg connection.execute( text( "CREATE TEMP TABLE tmp_enriched_pnos(" - " id INTEGER PRIMARY KEY," - " is_at_port BOOLEAN," - " meters_from_previous_position REAL," - " time_since_previous_position INTERVAL," - " average_speed REAL," - " is_fishing BOOLEAN," - " time_emitting_at_sea INTERVAL" + " logbook_reports_pno_id INTEGER PRIMARY KEY," + " enriched BOOLEAN," + " trip_gears JSONB," + " pno_types JSONB," + " trip_segments JSONB" ")" "ON COMMIT DROP;" ) @@ -129,16 +337,14 @@ def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logg enriched_pnos = prepare_df_for_loading( enriched_pnos, logger, + jsonb_columns=["trip_gears", "pno_types", "trip_segments"], ) columns_to_load = [ - "id", - "is_at_port", - "meters_from_previous_position", - "time_since_previous_position", - "average_speed", - "is_fishing", - "time_emitting_at_sea", + "logbook_reports_pno_id", + "trip_gears", + "pno_types", + "trip_segments", ] logger.info("Loading to temporary table") @@ -155,33 +361,17 @@ def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logg connection.execute( text( - "UPDATE public.logbook p " - "SET " - " is_at_port = ep.is_at_port, " - " meters_from_previous_position = COALESCE( " - " ep.meters_from_previous_position, " - " p.meters_from_previous_position " - " ), " - " time_since_previous_position = COALESCE( " - " ep.time_since_previous_position, " - " p.time_since_previous_position " - " ), " - " average_speed = COALESCE( " - " ep.average_speed, " - " p.average_speed " - " ), " - " is_fishing = COALESCE( " - " ep.is_fishing, " - " p.is_fishing " - " )," - " time_emitting_at_sea = COALESCE( " - " ep.time_emitting_at_sea, " - " p.time_emitting_at_sea " - " )" + "UPDATE public.logbook_reports r " + "SET" + " enriched = true," + " trip_gears = COALESCE(ep.trip_gears, '[]'::jsonb)," + " pno_types = COALESCE(ep.pno_types, '[]'::jsonb)," + " trip_segments = COALESCE(ep.trip_segments, '[]'::jsonb) " "FROM tmp_enriched_pnos ep " - "WHERE p.id = ep.id " - "AND p.date_time >= :start " - "AND p.date_time <= :end;" + "WHERE r.id = ep.logbook_reports_pno_id " + "AND r.operation_datetime_utc >= :start " + "AND r.operation_datetime_utc <= :end " + "AND log_type = 'PNO';" ), { "start": period.start, @@ -205,21 +395,29 @@ def extract_enrich_load_logbook( logger = prefect.context.get("logger") logger.info(f"Processing pnos for period {period.start} - {period.end}.") - trips_period = extract_pno_trips_period() + trips_period = extract_pno_trips_period(period) - logger.info("Extracting PNO...") + logger.info("Extracting PNOs...") pnos_species_and_gears = extract_pno_species_and_gears( pno_emission_period=period, trips_period=trips_period ) logger.info( - f"Extracted {len(pnos_species_and_gears)} PNO species from {pnos_species_and_gears.id.nunique()} PNOs." + f"Extracted {len(pnos_species_and_gears)} PNO species from {pnos_species_and_gears.logbook_reports_pno_id.nunique()} PNOs." ) logger.info("Computing PNO segments...") - pnos = compute_pno_segments( + pnos_with_segments = compute_pno_segments( pno_species_and_gears=pnos_species_and_gears, segments=segments ) + logger.info("Computing PNO types...") + pnos_with_types = compute_pno_types( + pno_species_and_gears=pnos_species_and_gears, pno_types=pno_types + ) + + logger.info("Merging PNO types and segments...") + pnos = merge_segments_and_types(pnos_with_types, pnos_with_segments) + logger.info("Loading") load_enriched_pnos(pnos, period, logger) diff --git a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py index 993e76cbf9..777ce7e39e 100644 --- a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py +++ b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py @@ -1,17 +1,25 @@ from datetime import datetime +from logging import Logger import pandas as pd import pytest import pytz +from sqlalchemy import text +from src.db_config import create_engine from src.pipeline.flows.enrich_logbook import ( + compute_pno_segments, compute_pno_types, extract_pno_species_and_gears, extract_pno_trips_period, extract_pno_types, flow, + load_enriched_pnos, + merge_segments_and_types, + reset_pnos, ) from src.pipeline.helpers.dates import Period +from src.read_query import read_query from tests.mocks import mock_check_flow_not_running flow.replace(flow.get_tasks("check_flow_not_running")[0], mock_check_flow_not_running) @@ -193,6 +201,293 @@ def expected_pno_species_and_gears() -> pd.DataFrame: ) +@pytest.fixture +def segments() -> pd.DataFrame: + return pd.DataFrame( + { + "year": [2023, 2023, 2023, 2023, 2015], + "segment": ["SOTM", "SHKE27", "SSB", "SxTB8910", "SxTB8910-2015"], + "segment_name": [ + "Chaluts pélagiques", + "Merlu en zone 27", + "Senne de plage", + "Merlu Morue xTB zones 8 9 10", + "Merlu Morue xTB zones 8 9 10 (2015)", + ], + "gears": [ + ["OTM", "PTM"], + [], + ["SB"], + ["OTB", "PTB"], + ["OTB", "PTB"], + ], + "fao_areas": [ + [], + ["27"], + [], + ["27.8", "27.9", "27.10"], + ["27.8", "27.9", "27.10"], + ], + "species": [ + [], + ["HKE"], + [], + ["HKE", "COD"], + ["HKE", "COD"], + ], + } + ) + + +@pytest.fixture +def expected_computed_pno_types() -> pd.DataFrame: + return pd.DataFrame( + { + "logbook_reports_pno_id": [1, 2, 3, 4, 5, 6, 7, 8], + "trip_gears": [ + [ + {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, + {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, + ], + [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], + None, + [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], + [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], + [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], + [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], + [{"gear": "SB", "mesh": 20, "dimensions": "4.5"}], + ], + "pno_types": [ + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + } + ], + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 2", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + [ + { + "pno_type_name": "Préavis par pavillon", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + } + ], + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 2", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + None, + None, + [ + { + "pno_type_name": "Préavis par engin", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + } + ], + ], + } + ) + + +@pytest.fixture +def expected_computed_pno_segments() -> pd.DataFrame: + return pd.DataFrame( + { + "logbook_reports_pno_id": [1, 2, 3, 4, 5, 6, 7, 8], + "trip_segments": [ + None, + None, + None, + [ + { + "segment": "SxTB8910", + "segment_name": "Merlu Morue xTB zones 8 9 10", + } + ], + [{"segment": "SOTM", "segment_name": "Chaluts pélagiques"}], + [ + {"segment": "SHKE27", "segment_name": "Merlu en zone 27"}, + {"segment": "SOTM", "segment_name": "Chaluts pélagiques"}, + ], + [{"segment": "SOTM", "segment_name": "Chaluts pélagiques"}], + [{"segment": "SSB", "segment_name": "Senne de plage"}], + ], + } + ) + + +@pytest.fixture +def pnos_to_load() -> pd.DataFrame: + return pd.DataFrame( + { + "logbook_reports_pno_id": [12, 13], + "trip_gears": [ + [ + {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, + {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, + ], + None, + ], + "pno_types": [ + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 2", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + None, + ], + "trip_segments": [ + None, + [ + {"segment": "SHKE27", "segment_name": "Merlu en zone 27"}, + {"segment": "SOTM", "segment_name": "Chaluts pélagiques"}, + ], + ], + } + ) + + +@pytest.fixture +def expected_merged_pnos() -> pd.DataFrame: + return pd.DataFrame( + { + "logbook_reports_pno_id": [1, 2, 3, 4, 5, 6, 7, 8], + "trip_gears": [ + [ + {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, + {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, + ], + [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], + None, + [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], + [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], + [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], + [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], + [{"gear": "SB", "mesh": 20, "dimensions": "4.5"}], + ], + "pno_types": [ + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + } + ], + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 2", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + [ + { + "pno_type_name": "Préavis par pavillon", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + } + ], + [ + { + "pno_type_name": "Préavis type 1", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + { + "pno_type_name": "Préavis type 2", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + }, + ], + None, + None, + [ + { + "pno_type_name": "Préavis par engin", + "minimum_notification_period": 4.0, + "has_designated_ports": True, + } + ], + ], + "trip_segments": [ + None, + None, + None, + [ + { + "segment": "SxTB8910", + "segment_name": "Merlu Morue xTB zones 8 9 10", + } + ], + [{"segment": "SOTM", "segment_name": "Chaluts pélagiques"}], + [ + {"segment": "SHKE27", "segment_name": "Merlu en zone 27"}, + {"segment": "SOTM", "segment_name": "Chaluts pélagiques"}, + ], + [{"segment": "SOTM", "segment_name": "Chaluts pélagiques"}], + [{"segment": "SSB", "segment_name": "Senne de plage"}], + ], + } + ) + + def test_extract_pno_types(reset_test_data, expected_pno_types): pno_types = extract_pno_types.run() pd.testing.assert_frame_equal(pno_types, expected_pno_types) @@ -229,23 +524,153 @@ def test_extract_pno_species_and_gears(reset_test_data, expected_pno_species_and ) -def test_compute_pno_types(expected_pno_types, sample_pno_species_and_gears): - compute_pno_types(sample_pno_species_and_gears, expected_pno_types) +def test_compute_pno_types( + expected_pno_types, sample_pno_species_and_gears, expected_computed_pno_types +): + res = compute_pno_types(sample_pno_species_and_gears, expected_pno_types) + pd.testing.assert_frame_equal(res, expected_computed_pno_types) + + +def test_compute_pno_segments( + reset_test_data, + sample_pno_species_and_gears, + segments, + expected_computed_pno_segments, +): + res = compute_pno_segments(sample_pno_species_and_gears, segments) + pd.testing.assert_frame_equal(res, expected_computed_pno_segments) -def test_load_then_reset_logbook(reset_test_data): - # pnos = read_query("SELECT * FROM logbook_reports WHERE log_type = 'PNO'", db="monitorfish_remote") - # breakpoint() - pass +def test_merge_segments_and_types( + expected_computed_pno_types, expected_computed_pno_segments, expected_merged_pnos +): + res = merge_segments_and_types( + expected_computed_pno_types, expected_computed_pno_segments + ) + pd.testing.assert_frame_equal(res, expected_merged_pnos) + +def test_load_then_reset_logbook(reset_test_data, pnos_to_load): + query = "SELECT * FROM logbook_reports WHERE log_type = 'PNO' ORDER BY id" + initial_pnos = read_query(query, db="monitorfish_remote") + pno_period = Period( + start=datetime(2020, 5, 6, 18, 30, 0), end=datetime(2020, 5, 6, 18, 50, 0) + ) + logger = Logger("myLogger") + load_enriched_pnos(enriched_pnos=pnos_to_load, period=pno_period, logger=logger) + final_pnos = read_query(query, db="monitorfish_remote") -def test_extract_enrich_load(reset_test_data): - pass + assert not initial_pnos.enriched.any() + assert not final_pnos.loc[final_pnos.id == 8, "enriched"].values[0] + assert final_pnos.loc[final_pnos.id.isin([12, 13]), "enriched"].all() + pd.testing.assert_frame_equal( + final_pnos.loc[ + final_pnos.enriched, ["id", "trip_gears", "pno_types", "trip_segments"] + ].reset_index(drop=True), + pnos_to_load.rename(columns={"logbook_reports_pno_id": "id"}), + ) + + # Reset logbook and check that the logbook_reports table is back to its original + # state. + reset_pnos.run(pno_period) + pnos_after_reset = read_query(query, db="monitorfish_remote") + pd.testing.assert_frame_equal(pnos_after_reset, initial_pnos) + + +def test_flow(reset_test_data): + query = ( + "SELECT id, enriched, trip_gears, pno_types, trip_segments " + "FROM logbook_reports WHERE log_type = 'PNO' ORDER BY id" + ) + + initial_pnos = read_query(query, db="monitorfish_remote") + + now = datetime.utcnow() + pno_start_date = datetime(2020, 5, 5) + pno_end_date = datetime(2020, 5, 7) + + start_hours_ago = int((now - pno_start_date).total_seconds() / 3600) + end_hours_ago = int((now - pno_end_date).total_seconds() / 3600) + minutes_per_chunk = 2 * 24 * 60 + + # First run + state = flow.run( + start_hours_ago=start_hours_ago, + end_hours_ago=end_hours_ago, + minutes_per_chunk=minutes_per_chunk, + recompute_all=False, + ) + assert state.is_successful() + + pnos_after_first_run = read_query(query, db="monitorfish_remote") + + # Manual update : reset PNO n°12, modify PNO n°13 + e = create_engine("monitorfish_remote") + with e.begin() as conn: + conn.execute( + text("UPDATE logbook_reports " "SET enriched = false " "WHERE id = 12;") + ) + + conn.execute( + text( + "UPDATE logbook_reports " + """SET trip_gears = '[{"gear": "This was set manually"}]'::jsonb """ + "WHERE id = 13;" + ) + ) + + # Second run without reset : manual modifications on PNO n°13 should be preserved + state = flow.run( + start_hours_ago=start_hours_ago, + end_hours_ago=end_hours_ago, + minutes_per_chunk=minutes_per_chunk, + recompute_all=False, + ) + assert state.is_successful() + + pnos_after_second_run_without_reset = read_query(query, db="monitorfish_remote") + + # Third run with reset : manual modifications on PNO n°13 should be erased and + # recomputed. + state = flow.run( + start_hours_ago=start_hours_ago, + end_hours_ago=end_hours_ago, + minutes_per_chunk=minutes_per_chunk, + recompute_all=True, + ) + assert state.is_successful() + + pnos_after_third_run_with_reset = read_query(query, db="monitorfish_remote") + + # Initially no PNO should be enriched + assert ( + not initial_pnos[["enriched", "trip_gears", "pno_types", "trip_segments"]] + .any() + .any() + ) + # After first run PNO with ids 12 and 13 should be enriched + assert set(pnos_after_first_run.loc[pnos_after_first_run.enriched, "id"]) == { + 12, + 13, + } + pnos_after_first_run.loc[pnos_after_first_run.id == 12, "trip_gears"].iloc[0] == [ + {"gear": "TBB", "mesh": 140, "dimensions": "250.0"} + ] -def test_flow_does_not_recompute_all_when_not_asked_to(reset_test_data): - pass + pnos_after_first_run.loc[pnos_after_first_run.id == 13, "trip_gears"].iloc[0] == [ + {"gear": "TBB", "mesh": 140, "dimensions": "250.0"} + ] + # After second run without reset, manual modifications on PNO n°13 should be + # preserved + assert pnos_after_second_run_without_reset.loc[ + pnos_after_second_run_without_reset.id == 12, "trip_gears" + ].iloc[0] == [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}] + assert pnos_after_second_run_without_reset.loc[ + pnos_after_second_run_without_reset.id == 13, "trip_gears" + ].iloc[0] == [{"gear": "This was set manually"}] -def test_flow_recomputes_all_when_asked_to(reset_test_data): - pass + # After third run with reset, manual modifications on PNO n°13 should be erased and + # recomputed. + pd.testing.assert_frame_equal(pnos_after_first_run, pnos_after_third_run_with_reset) From 9c0cef62397efaa736cf4b86a2056a9ac659a0d7 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Mar 2024 09:23:02 +0100 Subject: [PATCH 16/82] Update backend pno_types test data --- .../testdata/V666.27__Insert_dummy_pno_types.sql | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql b/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql index e36aabffe9..d9c4f985ab 100644 --- a/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql +++ b/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql @@ -1,10 +1,11 @@ INSERT INTO public.pno_types ( - id name, minimum_notification_period, has_designated_ports) VALUES - ( 1, 'Préavis type 1', 4, true), - ( 2, 'Préavis type 2', 4, true), - (10, 'Préavis par pavillon', 4, true), - (12, 'Préavis par engin', 4, true); + id name, minimum_notification_period, has_designated_ports) VALUES + (1, 'Préavis type 1', 4, true), + (2, 'Préavis type 2', 4, true), + (3, 'Préavis par pavillon', 4, true), + (4, 'Préavis par engin', 4, true); +ALTER SEQUENCE pno_types_id_seq RESTART WITH 5; INSERT INTO public.pno_type_rules ( pno_type_id, species, fao_areas, cgpm_areas, gears, flag_states, minimum_quantity_kg) VALUES @@ -13,5 +14,5 @@ INSERT INTO public.pno_type_rules ( ( 1, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), ( 2, '{HKE,BSS,COD,ANF,SOL}', '{27.3.a,27.4,27.6,27.7,27.8.a,27.8.b,27.8.c,27.8.d,27.9.a}', '{}', '{}', '{}', 2000), ( 2, '{HER,MAC,HOM,WHB}', '{27,34.1.2,34.2}', '{}', '{}', '{}', 10000), - ( 10, '{}', '{}', '{}', '{}', '{GBR,VEN}', 0), - ( 12, '{}', '{}', '{}', '{SB}', '{}', 0); \ No newline at end of file + ( 3, '{}', '{}', '{}', '{}', '{GBR,VEN}', 0), + ( 4, '{}', '{}', '{}', '{SB}', '{}', 0); \ No newline at end of file From 873fda5bc325bf51db5859a5b7fdcb515fd8b3d1 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Mar 2024 12:28:10 +0100 Subject: [PATCH 17/82] Set empty list instead of null when enriching logbook_reports --- .../src/pipeline/flows/enrich_logbook.py | 13 +++-- .../test_flows/test_enrich_logbook.py | 57 ++++++++++++++++--- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/datascience/src/pipeline/flows/enrich_logbook.py b/datascience/src/pipeline/flows/enrich_logbook.py index 78847edbbb..6160b80bff 100644 --- a/datascience/src/pipeline/flows/enrich_logbook.py +++ b/datascience/src/pipeline/flows/enrich_logbook.py @@ -358,15 +358,20 @@ def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logg ) logger.info("Updating pnos from temporary table") - connection.execute( text( "UPDATE public.logbook_reports r " "SET" " enriched = true," - " trip_gears = COALESCE(ep.trip_gears, '[]'::jsonb)," - " pno_types = COALESCE(ep.pno_types, '[]'::jsonb)," - " trip_segments = COALESCE(ep.trip_segments, '[]'::jsonb) " + " trip_gears = CASE " + " WHEN ep.trip_gears = 'null' THEN '[]'::jsonb " + " ELSE ep.trip_gears END, " + " pno_types = CASE " + " WHEN ep.pno_types = 'null' THEN '[]'::jsonb " + " ELSE ep.pno_types END, " + " trip_segments = CASE " + " WHEN ep.trip_segments = 'null' THEN '[]'::jsonb " + " ELSE ep.trip_segments END " "FROM tmp_enriched_pnos ep " "WHERE r.id = ep.logbook_reports_pno_id " "AND r.operation_datetime_utc >= :start " diff --git a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py index 777ce7e39e..9fc4d8e787 100644 --- a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py +++ b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py @@ -387,6 +387,48 @@ def pnos_to_load() -> pd.DataFrame: ) +@pytest.fixture +def expected_loaded_pnos() -> pd.DataFrame: + return pd.DataFrame( + { + "id": [8, 12, 13], + "enriched": [False, True, True], + "trip_gears": [ + None, + [ + {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, + {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, + ], + [], + ], + "pno_types": [ + None, + [ + { + "pno_type_name": "Préavis type 1", + "has_designated_ports": True, + "minimum_notification_period": 4.0, + }, + { + "pno_type_name": "Préavis type 2", + "has_designated_ports": True, + "minimum_notification_period": 4.0, + }, + ], + [], + ], + "trip_segments": [ + None, + [], + [ + {"segment": "SHKE27", "segment_name": "Merlu en zone 27"}, + {"segment": "SOTM", "segment_name": "Chaluts pélagiques"}, + ], + ], + } + ) + + @pytest.fixture def expected_merged_pnos() -> pd.DataFrame: return pd.DataFrame( @@ -550,8 +592,11 @@ def test_merge_segments_and_types( pd.testing.assert_frame_equal(res, expected_merged_pnos) -def test_load_then_reset_logbook(reset_test_data, pnos_to_load): - query = "SELECT * FROM logbook_reports WHERE log_type = 'PNO' ORDER BY id" +def test_load_then_reset_logbook(reset_test_data, pnos_to_load, expected_loaded_pnos): + query = ( + "SELECT id, enriched, trip_gears, pno_types, trip_segments " + "FROM logbook_reports WHERE log_type = 'PNO' ORDER BY id" + ) initial_pnos = read_query(query, db="monitorfish_remote") pno_period = Period( start=datetime(2020, 5, 6, 18, 30, 0), end=datetime(2020, 5, 6, 18, 50, 0) @@ -563,12 +608,8 @@ def test_load_then_reset_logbook(reset_test_data, pnos_to_load): assert not initial_pnos.enriched.any() assert not final_pnos.loc[final_pnos.id == 8, "enriched"].values[0] assert final_pnos.loc[final_pnos.id.isin([12, 13]), "enriched"].all() - pd.testing.assert_frame_equal( - final_pnos.loc[ - final_pnos.enriched, ["id", "trip_gears", "pno_types", "trip_segments"] - ].reset_index(drop=True), - pnos_to_load.rename(columns={"logbook_reports_pno_id": "id"}), - ) + + pd.testing.assert_frame_equal(final_pnos, expected_loaded_pnos) # Reset logbook and check that the logbook_reports table is back to its original # state. From 079b76dfc207fb7195522feaa9e472e07f0515bc Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Mar 2024 12:28:56 +0100 Subject: [PATCH 18/82] Add enriched logbook_reports backend test data --- .../V666.27__Insert_dummy_pno_types.sql | 2 +- .../db/testdata/V666.5__Insert_logbook.sql | 97 ++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql b/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql index d9c4f985ab..43ae469392 100644 --- a/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql +++ b/backend/src/main/resources/db/testdata/V666.27__Insert_dummy_pno_types.sql @@ -1,5 +1,5 @@ INSERT INTO public.pno_types ( - id name, minimum_notification_period, has_designated_ports) VALUES + id, name, minimum_notification_period, has_designated_ports) VALUES (1, 'Préavis type 1', 4, true), (2, 'Préavis type 2', 4, true), (3, 'Préavis par pavillon', 4, true), diff --git a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql index 19db3ae758..f0581c7abe 100644 --- a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql +++ b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql @@ -242,6 +242,7 @@ VALUES ('OOF20190126059903', 'Message FLUX xml'); INSERT INTO logbook_reports (operation_number, analyzed_by_rules, trip_number, operation_country, @@ -448,8 +449,8 @@ VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-10-20T12:16:00Z"}', '2021-01-18T07:17:26.736456Z', 'ERS', 'TurboCatch (3.7-1)'), - ('OOF20191011059902', 9463715, 'OOF', '2019-10-21T08:16:00Z', 'DAT', 'OOF20191011059902', null, - '2019-10-11T08:16:00Z', + ('OOF20191011059902', 9463715, 'OOF', (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', 'DAT', 'OOF20191011059902', null, + (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', '2021-01-18T07:17:19.04244Z', 'ERS', 'TurboCatch (3.7-1)'), @@ -602,9 +603,22 @@ UPDATE logbook_reports SET value = jsonb_set(value, '{predictedArrivalDatetimeUtc}', concat('"', to_char( (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', 'YYYY-MM-DD"T"HH24:MI:SSZ'), '"')::jsonb), + operation_datetime_utc = (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', report_datetime_utc = (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes' WHERE operation_number = 'OOF20191011059902'; +UPDATE logbook_reports +SET value = jsonb_set( + value, + '{tripStartDate}', + concat( + '"', + to_char((now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '72 hours', 'YYYY-MM-DDT00:00:00Z'), + '"' + )::jsonb +) +WHERE operation_number = 'OOF20191011059902'; + UPDATE logbook_reports SET value = jsonb_set(value, '{returnDatetimeUtc}', concat('"', to_char( (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '22 hours 10 minutes', 'YYYY-MM-DD"T"HH24:MI:SSZ'), @@ -650,7 +664,7 @@ VALUES ('OOF20190439686456', 20230086, 'OOF', CURRENT_DATE - INTERVAL '5 days', 'FR263454484', 'FE4864', '8FR6541', 'NO NAME', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2018-02-17T00:00Z", "departureDatetimeUtc": "2018-02-17T01:05Z"}', CURRENT_DATE - INTERVAL '4 days', 'ERS', 'TurboCatch (3.7-1)'), - ('OOF20190439686456', 20230087, 'OOF', CURRENT_DATE - INTERVAL '3 days', 'DAT', 'OOF20190439686456', null, + ('OOF20190439686457', 20230087, 'OOF', CURRENT_DATE - INTERVAL '3 days', 'DAT', 'OOF20190439686457', null, CURRENT_DATE - INTERVAL '3 days', 'FR263454484', 'FE4864', '8FR6541', 'NO NAME', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 25.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2018-02-20T00:00Z", "predictedArrivalDatetimeUtc": "2018-02-20T13:38Z"}', @@ -660,3 +674,80 @@ VALUES ('OOF20190439686456', 20230086, 'OOF', CURRENT_DATE - INTERVAL '5 days', 'PNO', '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z"}', CURRENT_DATE - INTERVAL '2 days', 'FLUX', null); + + + +UPDATE logbook_reports +SET value = jsonb_set( + value, + '{predictedArrivalDatetimeUtc}', + concat( + '"', + to_char(CURRENT_DATE AT TIME ZONE 'UTC' - interval '2 days 20 hours', 'YYYY-MM-DD"T"HH24:MI:SSZ'), + '"' + )::jsonb +) +WHERE operation_number = 'OOF20190439686457'; + +UPDATE logbook_reports +SET value = jsonb_set( + value, + '{tripStartDate}', + concat( + '"', + to_char(CURRENT_DATE AT TIME ZONE 'UTC' - interval '5 days', 'YYYY-MM-DDT00:00:00Z'), + '"' + )::jsonb +) +WHERE operation_number = 'OOF20190439686457'; + +UPDATE logbook_reports +SET value = jsonb_set( + value, + '{predictedArrivalDatetimeUtc}', + concat( + '"', + to_char(CURRENT_DATE AT TIME ZONE 'UTC' - interval '1 days 12 hours', 'YYYY-MM-DD"T"HH24:MI:SSZ'), + '"' + )::jsonb +) +WHERE operation_number = 'd5c3b039-aaee-4cca-bcae-637f5fe574f5'; + +UPDATE logbook_reports +SET value = jsonb_set( + value, + '{tripStartDate}', + concat( + '"', + to_char(CURRENT_DATE AT TIME ZONE 'UTC' - interval '6 days', 'YYYY-MM-DDT00:00:00Z'), + '"' + )::jsonb +) +WHERE operation_number = 'd5c3b039-aaee-4cca-bcae-637f5fe574f5'; + +UPDATE logbook_reports +SET + enriched = true, + trip_gears = '[]'::jsonb, + trip_segments = '[]'::jsonb, + pno_types = '[]'::jsonb +WHERE operation_number IN ('OOF20191011059902', 'OOF20190439686457', 'd5c3b039-aaee-4cca-bcae-637f5fe574f5'); + +UPDATE logbook_reports +SET + enriched = true, + trip_gears = '[{"gear": "GTR", "mesh": 100, "dimensions": "250;180"}, {"gear": "GTR", "mesh": 120.5, "dimensions": "250;280"}]'::jsonb, + trip_segments = '[{"segment": "NWW01", "segment_name": "Chalutiers de fond"}, {"segment": "PEL01", "segment_name": "Chalutiers pélagiques"}]'::jsonb, + pno_types = '[ + { + "pno_type_name": "Préavis type X", + "minimum_notification_period": 4.0, + "has_designated_ports": false + }, + { + "pno_type_name": "Préavis type Y", + "minimum_notification_period": 8.0, + "has_designated_ports": true + } + ]'::jsonb +WHERE operation_number = 'OOF20191011059902'; From e8b59f2e03c3d26d225b2d8c4e1f092b7ba287fe Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Mar 2024 13:01:07 +0100 Subject: [PATCH 19/82] Update test data --- .../main/resources/db/testdata/V666.5__Insert_logbook.sql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql index f0581c7abe..3dd9ad729e 100644 --- a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql +++ b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql @@ -449,8 +449,8 @@ VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-10-20T12:16:00Z"}', '2021-01-18T07:17:26.736456Z', 'ERS', 'TurboCatch (3.7-1)'), - ('OOF20191011059902', 9463715, 'OOF', (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', 'DAT', 'OOF20191011059902', null, - (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', + ('OOF20191011059902', 9463715, 'OOF', '2019-10-21T08:16:00Z', 'DAT', 'OOF20191011059902', null, + '2019-10-21T08:16:00Z', 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', '2021-01-18T07:17:19.04244Z', 'ERS', 'TurboCatch (3.7-1)'), @@ -603,7 +603,6 @@ UPDATE logbook_reports SET value = jsonb_set(value, '{predictedArrivalDatetimeUtc}', concat('"', to_char( (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', 'YYYY-MM-DD"T"HH24:MI:SSZ'), '"')::jsonb), - operation_datetime_utc = (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes', report_datetime_utc = (now() AT TIME ZONE 'UTC')::TIMESTAMP - interval '24 hours 30 minutes' WHERE operation_number = 'OOF20191011059902'; From 60c52fc4213b60bfa86a586320993394dbdc5771 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Mar 2024 13:10:50 +0100 Subject: [PATCH 20/82] Update test data --- .../src/main/resources/db/testdata/V666.5__Insert_logbook.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql index 3dd9ad729e..50888bc7f3 100644 --- a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql +++ b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql @@ -449,7 +449,7 @@ VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-10-20T12:16:00Z"}', '2021-01-18T07:17:26.736456Z', 'ERS', 'TurboCatch (3.7-1)'), - ('OOF20191011059902', 9463715, 'OOF', '2019-10-21T08:16:00Z', 'DAT', 'OOF20191011059902', null, + ('OOF20191011059902', 9463715, 'OOF', '2019-10-11T08:16:00Z', 'DAT', 'OOF20191011059902', null, '2019-10-21T08:16:00Z', 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', From 81a5e072f340f1ea005fdbd71e91e2a7adc11b87 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 15 Feb 2024 22:36:24 +0100 Subject: [PATCH 21/82] Add prior notification list & filters in Frontend --- frontend/.env.example | 1 + frontend/.env.local.defaults | 1 + .../mappings/get-prior_notifications.v1.json | 35 + frontend/package-lock.json | 879 +----------------- frontend/package.json | 2 + frontend/src/api/api.ts | 1 + .../constants/{constants.js => constants.ts} | 15 +- frontend/src/constants/index.ts | 10 +- .../domain/entities/{global.js => global.ts} | 0 .../src/domain/entities/seaFront/constants.ts | 1 + .../domain/entities/sideWindow/constants.ts | 15 +- frontend/src/domain/types/Gear.ts | 1 + frontend/src/domain/types/port.ts | 1 + frontend/src/domain/types/specy.ts | 1 + frontend/src/env.d.ts | 1 + frontend/src/features/FleetSegment/types.ts | 1 + .../ActionForm/shared/GearsField.tsx | 4 +- .../PriorNotification.types.ts | 57 ++ .../src/features/PriorNotification/api.ts | 15 + .../PriorNotificationList/ButtonsGroupRow.tsx | 24 + .../PriorNotificationList/FilterBar.tsx | 292 ++++++ .../PriorNotificationList/constants.tsx | 351 +++++++ .../PriorNotificationList/index.tsx | 301 ++++++ .../components/PriorNotificationList/types.ts | 25 + .../src/features/PriorNotification/slice.ts | 45 + .../src/features/SideWindow/Menu/index.tsx | 14 + .../src/features/SideWindow/SubMenu/index.tsx | 5 + .../features/SideWindow/components/Body.tsx | 8 + .../features/SideWindow/components/Header.tsx | 22 + .../features/SideWindow/components/Page.tsx | 11 + frontend/src/features/SideWindow/index.tsx | 15 +- frontend/src/features/Station/slice.ts | 4 +- .../features/VesselList/VesselListFilters.tsx | 12 +- .../src/hooks/useGetFleetSegmentsAsOptions.ts | 36 + .../src/hooks/useGetGearsAsTreeOptions.ts | 43 + .../src/hooks/useGetPortsAsTreeOptions.ts | 43 + frontend/src/hooks/useGetSpeciesAsOptions.ts | 36 + frontend/src/hooks/useTable/types.ts | 3 +- frontend/src/store/reducers.ts | 3 + frontend/src/types.ts | 12 +- frontend/src/ui/NoRsuiteOverrideWrapper.tsx | 10 +- frontend/src/utils/nullify.ts | 2 +- frontend/src/utils/undefinedize.ts | 2 +- infra/docker/docker-compose.cypress.yml | 1 + infra/docker/docker-compose.puppeteer.yml | 1 + 45 files changed, 1464 insertions(+), 898 deletions(-) create mode 100644 frontend/cypress/mappings/get-prior_notifications.v1.json rename frontend/src/constants/{constants.js => constants.ts} (91%) rename frontend/src/domain/entities/{global.js => global.ts} (100%) create mode 100644 frontend/src/features/PriorNotification/PriorNotification.types.ts create mode 100644 frontend/src/features/PriorNotification/api.ts create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/ButtonsGroupRow.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts create mode 100644 frontend/src/features/PriorNotification/slice.ts create mode 100644 frontend/src/features/SideWindow/components/Body.tsx create mode 100644 frontend/src/features/SideWindow/components/Header.tsx create mode 100644 frontend/src/features/SideWindow/components/Page.tsx create mode 100644 frontend/src/hooks/useGetFleetSegmentsAsOptions.ts create mode 100644 frontend/src/hooks/useGetGearsAsTreeOptions.ts create mode 100644 frontend/src/hooks/useGetPortsAsTreeOptions.ts create mode 100644 frontend/src/hooks/useGetSpeciesAsOptions.ts diff --git a/frontend/.env.example b/frontend/.env.example index a5a44bbe8e..2c297763af 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -3,6 +3,7 @@ FRONTEND_MISSION_FORM_AUTO_SAVE_ENABLED= FRONTEND_MISSION_FORM_AUTO_UPDATE_ENABLED= +FRONTEND_PRIOR_NOTIFICATION_LIST_ENABLED= ################################################################################ # GeoServer diff --git a/frontend/.env.local.defaults b/frontend/.env.local.defaults index 87610917ec..a59217bdee 100644 --- a/frontend/.env.local.defaults +++ b/frontend/.env.local.defaults @@ -3,6 +3,7 @@ FRONTEND_MISSION_FORM_AUTO_SAVE_ENABLED=true FRONTEND_MISSION_FORM_AUTO_UPDATE_ENABLED=true +FRONTEND_PRIOR_NOTIFICATION_LIST_ENABLED=true ################################################################################ # GeoServer diff --git a/frontend/cypress/mappings/get-prior_notifications.v1.json b/frontend/cypress/mappings/get-prior_notifications.v1.json new file mode 100644 index 0000000000..eb692483ab --- /dev/null +++ b/frontend/cypress/mappings/get-prior_notifications.v1.json @@ -0,0 +1,35 @@ +{ + "request": { + "method": "GET", + "url": "/api/v1/prior_notifications" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*" + }, + "jsonBody": [ + { + "id": "1", + "alertCount": 2, + "arrivalPortCode": "FRLEH", + "estimatedTimeOfArrival": "2024-03-01T03:00:00Z", + "fleetSegments": ["ABC", "DEF"], + "isOk": true, + "scheduledTimeOfLanding": "2024-03-01T04:00:00Z", + "type": "NON_SUBMITTED", + "port": { + "name": "Port 1" + }, + "vessel": { + "name": "Vessel 1", + "riskFactor": { + "riskFactor": 0.5 + } + } + } + ] + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a46c546e9d..bbe9e4f3c9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,8 @@ "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", + "@tanstack/react-table": "8.11.7", + "@tanstack/react-virtual": "beta", "comlink": "4.4.1", "date-fns": "2.30.0", "dayjs": "1.11.10", @@ -1033,262 +1035,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", - "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", - "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", - "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", - "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", - "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", - "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", - "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", - "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", - "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", - "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", - "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", - "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", - "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", - "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", - "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", - "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", @@ -1305,102 +1051,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", - "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", - "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", - "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", - "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", - "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", - "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2864,6 +2514,37 @@ "styled-components": "^5.0.0 || ^6.0.0" } }, + "node_modules/@mtes-mct/monitor-ui/node_modules/@tanstack/react-table": { + "version": "8.9.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.7.tgz", + "integrity": "sha512-UKUekM8JNUyWbjT1q3s1GpH5OtBL9mJ4258Il23fsahvkh3ou9TuFVmqI0/UPiFROgHkRlCBDNPUhcsC9YPFgg==", + "dependencies": { + "@tanstack/table-core": "8.9.7" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/@mtes-mct/monitor-ui/node_modules/@tanstack/table-core": { + "version": "8.9.7", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.7.tgz", + "integrity": "sha512-lkhVcGDxa9GSoDFPkplPDvzsiUACPZrxT3U1edPs0DCMKFhBDgZ7d1DPd7cqHH0JoybfbQ/qiTQYOQBg8sinJg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3428,86 +3109,6 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.102.tgz", - "integrity": "sha512-CJDxA5Wd2cUMULj3bjx4GEoiYyyiyL8oIOu4Nhrs9X+tlg8DnkCm4nI57RJGP8Mf6BaXPIJkHX8yjcefK2RlDA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.102.tgz", - "integrity": "sha512-X5akDkHwk6oAer49oER0qZMjNMkLH3IOZaV1m98uXIasAGyjo5WH1MKPeMLY1sY6V6TrufzwiSwD4ds571ytcg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.102.tgz", - "integrity": "sha512-kJH3XtZP9YQdjq/wYVBeFuiVQl4HaC4WwRrIxAHwe2OyvrwUI43dpW3LpxSggBnxXcVCXYWf36sTnv8S75o2Gw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.102.tgz", - "integrity": "sha512-flQP2WDyCgO24WmKA1wjjTx+xfCmavUete2Kp6yrM+631IHLGnr17eu7rYJ/d4EnDBId/ytMyrnWbTVkaVrpbQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.102.tgz", - "integrity": "sha512-bQEQSnC44DyoIGLw1+fNXKVGoCHi7eJOHr8BdH0y1ooy9ArskMjwobBFae3GX4T1AfnrTaejyr0FvLYIb0Zkog==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.3.102", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.102.tgz", @@ -3540,54 +3141,6 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.102.tgz", - "integrity": "sha512-w76JWLjkZNOfkB25nqdWUNCbt0zJ41CnWrJPZ+LxEai3zAnb2YtgB/cCIrwxDebRuMgE9EJXRj7gDDaTEAMOOQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.102.tgz", - "integrity": "sha512-vlDb09HiGqKwz+2cxDS9T5/461ipUQBplvuhW+cCbzzGuPq8lll2xeyZU0N1E4Sz3MVdSPx1tJREuRvlQjrwNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.102.tgz", - "integrity": "sha512-E/jfSD7sShllxBwwgDPeXp1UxvIqehj/ShSUqq1pjR/IDRXngcRSXKJK92mJkNFY7suH6BcCWwzrxZgkO7sWmw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -3630,11 +3183,11 @@ } }, "node_modules/@tanstack/react-table": { - "version": "8.9.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.7.tgz", - "integrity": "sha512-UKUekM8JNUyWbjT1q3s1GpH5OtBL9mJ4258Il23fsahvkh3ou9TuFVmqI0/UPiFROgHkRlCBDNPUhcsC9YPFgg==", + "version": "8.11.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.11.7.tgz", + "integrity": "sha512-ZbzfMkLjxUTzNPBXJYH38pv2VpC9WUA+Qe5USSHEBz0dysDTv4z/ARI3csOed/5gmlmrPzVUN3UXGuUMbod3Jg==", "dependencies": { - "@tanstack/table-core": "8.9.7" + "@tanstack/table-core": "8.11.7" }, "engines": { "node": ">=12" @@ -3664,10 +3217,10 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@tanstack/table-core": { - "version": "8.9.7", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.7.tgz", - "integrity": "sha512-lkhVcGDxa9GSoDFPkplPDvzsiUACPZrxT3U1edPs0DCMKFhBDgZ7d1DPd7cqHH0JoybfbQ/qiTQYOQBg8sinJg==", + "node_modules/@tanstack/table-core": { + "version": "8.11.7", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.11.7.tgz", + "integrity": "sha512-N3ksnkbPbsF3PjubuZCB/etTqvctpXWRHIXTmYfJFnhynQKjeZu8BCuHvdlLPpumKbA+bjY4Ay9AELYLOXPWBg==", "engines": { "node": ">=12" }, @@ -8639,20 +8192,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -16850,246 +16389,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/@esbuild/linux-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", @@ -17106,102 +16405,6 @@ "node": ">=12" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", diff --git a/frontend/package.json b/frontend/package.json index ab9ddfbb96..504d69011c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,8 @@ "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", + "@tanstack/react-table": "8.11.7", + "@tanstack/react-virtual": "beta", "comlink": "4.4.1", "date-fns": "2.30.0", "dayjs": "1.11.10", diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 84f082b985..4a78de76ad 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -96,6 +96,7 @@ export const monitorfishApi = createApi({ 'Infractions', 'Missions', 'MissionActions', + 'Notices', 'Ports', 'Species', 'RiskFactor', diff --git a/frontend/src/constants/constants.js b/frontend/src/constants/constants.ts similarity index 91% rename from frontend/src/constants/constants.js rename to frontend/src/constants/constants.ts index 6bfdf77dd9..cde7fff5b0 100644 --- a/frontend/src/constants/constants.js +++ b/frontend/src/constants/constants.ts @@ -1,31 +1,20 @@ export const COLORS = { // TODO Remove this color. blue: '#0A18DF', - charcoal: '#3B4559', - cultured: '#F7F7FA', - gainsboro: '#E5E5EB', - gunMetal: '#282F3E', - lightGray: '#CCCFD6', - maximumRed: '#E1000F', - mediumSeaGreen: '#29b361', - opal: '#a5bcc0', - - white: '#FFFFFF', - powderBlue: '#9ED7D9', - slateGray: '#707785', // TODO Remove this color (it's used as hex and not constant). titleBottomBorder: '#E0E0E0', - wheat: '#edd6a4' + wheat: '#edd6a4', + white: '#FFFFFF' } export const HIT_PIXEL_TO_TOLERANCE = 10 diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 50115213ca..f1bf86f7d9 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -1,10 +1,18 @@ -/* eslint-disable typescript-sort-keys/string-enum */ +import Countries from 'i18n-iso-countries' +import COUNTRIES_FR from 'i18n-iso-countries/langs/fr.json' import type { Option } from '@mtes-mct/monitor-ui' +Countries.registerLocale(COUNTRIES_FR) + export const BOOLEAN_AS_OPTIONS: Array> = [ { label: 'Oui', value: true }, { label: 'Non', value: false } ] export const FIVE_MINUTES = 5 * 60 * 1000 + +export const COUNTRIES_AS_OPTIONS: Option[] = Object.keys(Countries.getAlpha2Codes()).map(country => ({ + label: Countries.getName(country, 'fr'), + value: country.toLowerCase() +})) diff --git a/frontend/src/domain/entities/global.js b/frontend/src/domain/entities/global.ts similarity index 100% rename from frontend/src/domain/entities/global.js rename to frontend/src/domain/entities/global.ts diff --git a/frontend/src/domain/entities/seaFront/constants.ts b/frontend/src/domain/entities/seaFront/constants.ts index b3cdae7964..d2ea8c12ba 100644 --- a/frontend/src/domain/entities/seaFront/constants.ts +++ b/frontend/src/domain/entities/seaFront/constants.ts @@ -10,6 +10,7 @@ export enum SeaFrontGroup { SA = 'SA' } +// TODO This should not be dedicated to Mission feature here. export enum SeaFrontGroupLabel { ALL = 'Toutes les missions', MED = 'MED', diff --git a/frontend/src/domain/entities/sideWindow/constants.ts b/frontend/src/domain/entities/sideWindow/constants.ts index d8c3cdc8ed..dcd7a0fbbd 100644 --- a/frontend/src/domain/entities/sideWindow/constants.ts +++ b/frontend/src/domain/entities/sideWindow/constants.ts @@ -2,14 +2,15 @@ export enum SideWindowMenuKey { ALERT_LIST_AND_REPORTING_LIST = 'ALERT_LIST_AND_REPORTING_LIST', BEACON_MALFUNCTION_BOARD = 'BEACON_MALFUNCTION_BOARD', MISSION_FORM = 'MISSION_FORM', - MISSION_LIST = 'MISSION_LIST' + MISSION_LIST = 'MISSION_LIST', + PRIOR_NOTIFICATION_LIST = 'PRIOR_NOTIFICATION_LIST' } - -export enum SideWindowMenuLabel { - ALERT_LIST_AND_REPORTING_LIST = 'Alertes et signalements', - BEACON_MALFUNCTION_BOARD = 'Suivi VMS', - MISSION_FORM = 'Ajouter ou éditer une mission', - MISSION_LIST = 'Missions et contrôles' +export const SideWindowMenuLabel: Record = { + ALERT_LIST_AND_REPORTING_LIST: 'Alertes et signalements', + BEACON_MALFUNCTION_BOARD: 'Suivi VMS', + MISSION_FORM: 'Ajouter ou éditer une mission', + MISSION_LIST: 'Missions et contrôles', + PRIOR_NOTIFICATION_LIST: 'Préavis de débarquement' } export enum SideWindowStatus { diff --git a/frontend/src/domain/types/Gear.ts b/frontend/src/domain/types/Gear.ts index c6e90c34ec..3d1437d7a5 100644 --- a/frontend/src/domain/types/Gear.ts +++ b/frontend/src/domain/types/Gear.ts @@ -1,5 +1,6 @@ export type Gear = { category: string + /** ID. */ code: string groupId: string name: string diff --git a/frontend/src/domain/types/port.ts b/frontend/src/domain/types/port.ts index 3247bec3e3..0bcde4d911 100644 --- a/frontend/src/domain/types/port.ts +++ b/frontend/src/domain/types/port.ts @@ -1,6 +1,7 @@ export namespace Port { export interface Port { latitude: number | undefined + /** ID. */ locode: string longitude: number | undefined name: string diff --git a/frontend/src/domain/types/specy.ts b/frontend/src/domain/types/specy.ts index 397978d376..33e072ac0f 100644 --- a/frontend/src/domain/types/specy.ts +++ b/frontend/src/domain/types/specy.ts @@ -1,5 +1,6 @@ // TODO Exist both in Vessel and Specy. export type Specy = { + /** ID. */ code: string name: string } diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index 9f25f8e676..2054ce0768 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -12,6 +12,7 @@ interface ImportMetaEnv { readonly FRONTEND_OIDC_CLIENT_ID: string readonly FRONTEND_OIDC_ENABLED: string readonly FRONTEND_OIDC_REDIRECT_URI: string + readonly FRONTEND_PRIOR_NOTIFICATION_LIST_ENABLED: string readonly FRONTEND_SENTRY_DSN?: string readonly FRONTEND_SHOM_KEY: string } diff --git a/frontend/src/features/FleetSegment/types.ts b/frontend/src/features/FleetSegment/types.ts index aa898f6601..fe33cea9fc 100644 --- a/frontend/src/features/FleetSegment/types.ts +++ b/frontend/src/features/FleetSegment/types.ts @@ -6,6 +6,7 @@ export type FleetSegment = { gears: string[] | undefined impactRiskFactor: Float | undefined segment: string + // TODO Can this be undefined? segmentName: string | undefined targetSpecies: string[] | undefined year: number diff --git a/frontend/src/features/Mission/components/MissionForm/ActionForm/shared/GearsField.tsx b/frontend/src/features/Mission/components/MissionForm/ActionForm/shared/GearsField.tsx index 43cd274f6e..49b67a5f02 100644 --- a/frontend/src/features/Mission/components/MissionForm/ActionForm/shared/GearsField.tsx +++ b/frontend/src/features/Mission/components/MissionForm/ActionForm/shared/GearsField.tsx @@ -24,7 +24,7 @@ import type { MissionActionFormValues } from '../../types' import type { MissionAction } from '@features/Mission/missionAction.types' import type { Option } from '@mtes-mct/monitor-ui' import type { Gear } from 'domain/types/Gear' -import type { DeepPartial } from 'types' +import type { PartialDeep } from 'type-fest' export function GearsField() { const { values } = useFormikContext() @@ -44,7 +44,7 @@ export function GearsField() { })) }, [getGearsApiQuery.data]) - const typedError = meta.error as unknown as DeepPartial[] | undefined + const typedError = meta.error as unknown as PartialDeep[] | undefined const add = (newGear: Gear | undefined) => { if (!newGear) { diff --git a/frontend/src/features/PriorNotification/PriorNotification.types.ts b/frontend/src/features/PriorNotification/PriorNotification.types.ts new file mode 100644 index 0000000000..cf6e44e85a --- /dev/null +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -0,0 +1,57 @@ +import type { SeaFrontGroup } from '../../domain/entities/seaFront/constants' +import type { Vessel } from '../../domain/entities/vessel/types' +import type { Port } from '../../domain/types/port' +import type { FleetSegment } from '@features/FleetSegment/types' + +export namespace PriorNotification { + export interface PriorNotification { + alertCount: number + estimatedTimeOfArrival: string + facade: SeaFrontGroup + fleetSegments: FleetSegment[] + id: number + isSubmitted: boolean + port: Port.Port + reason: PriorNotificationReason + receivedAt: string + scheduledTimeOfLanding: string + types: PriorNotificationType[] + vessel: Vessel + } + + export enum PriorNotificationReason { + LANDING = 'LANDING' + } + export const PRIOR_NOTIFICATION_REASON_LABEL: Record = { + LANDING: 'Débarquement' + } + + /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/string-enum */ + export enum PriorNotificationType { + BASS = 'BASS', + DEEP_SEA_SPECIES = 'DEEP_SEA_SPECIES', + HAKE = 'HAKE', + SMALL_PELLAGIC_SPECIES = 'SMALL_PELLAGIC_SPECIES', + SOLE = 'SOLE', + BLUEFIN_THUMA = 'BLUEFIN_THUMA', + RED_CORAL = 'RED_CORAL', + SHORE_SEINE = 'SHORE_SEINE', + COMMUNITY = 'COMMUNITY', + THIRD_PARTY_VESSEL = 'THIRD_PARTY_VESSEL', + NOT_APPLICABLE = 'NOT_APPLICABLE' + } + export const PRIOR_NOTIFICATION_TYPE_LABEL: Record = { + BASS: 'Bar', + DEEP_SEA_SPECIES: 'Espèces eaux profondes', + HAKE: 'Merlu', + SMALL_PELLAGIC_SPECIES: 'Petits pélagiques', + SOLE: 'Sole', + BLUEFIN_THUMA: 'Thon rouge', + RED_CORAL: 'Corail rouge', + SHORE_SEINE: 'Senne de plage', + COMMUNITY: 'Navire communautaire', + THIRD_PARTY_VESSEL: 'Navire tiers', + NOT_APPLICABLE: 'Non soumis' + } + /* eslint-enable sort-keys-fix/sort-keys-fix */ +} diff --git a/frontend/src/features/PriorNotification/api.ts b/frontend/src/features/PriorNotification/api.ts new file mode 100644 index 0000000000..a72e46339f --- /dev/null +++ b/frontend/src/features/PriorNotification/api.ts @@ -0,0 +1,15 @@ +import { monitorenvApi } from '../../api/api' + +import type { PriorNotification } from './PriorNotification.types' + +// TODO Replace that (and uncomment tags). Temporarely using Fake Env API to use mappings. +export const priorNotificationApi = monitorenvApi.injectEndpoints({ + endpoints: builder => ({ + getNotices: builder.query({ + // providesTags: () => [{ type: 'Notices' }], + query: () => `/v1/prior_notifications` + }) + }) +}) + +export const { useGetNoticesQuery } = priorNotificationApi diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/ButtonsGroupRow.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/ButtonsGroupRow.tsx new file mode 100644 index 0000000000..ebcb3c30f5 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/ButtonsGroupRow.tsx @@ -0,0 +1,24 @@ +import { Accent, Icon, IconButton } from '@mtes-mct/monitor-ui' +import { noop } from 'lodash' +import styled from 'styled-components' + +export function ButtonsGroupRow({ id }) { + return ( + + noop(id)} /> + noop(id)} /> + + ) +} + +const ButtonsGroup = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + margin-top: 2px; + position: relative; + + > button { + padding: 0px; + } +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx new file mode 100644 index 0000000000..855fc635ba --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx @@ -0,0 +1,292 @@ +import { COUNTRIES_AS_OPTIONS } from '@constants/index' +import { PriorNotification } from '@features/PriorNotification/PriorNotification.types' +import { useGetFleetSegmentsAsOptions } from '@hooks/useGetFleetSegmentsAsOptions' +import { useGetGearsAsTreeOptions } from '@hooks/useGetGearsAsTreeOptions' +import { useGetPortsAsTreeOptions } from '@hooks/useGetPortsAsTreeOptions' +import { useGetSpeciesAsOptions } from '@hooks/useGetSpeciesAsOptions' +import { useMainAppDispatch } from '@hooks/useMainAppDispatch' +import { useMainAppSelector } from '@hooks/useMainAppSelector' +import { + DateRangePicker, + Icon, + MultiCascader, + MultiSelect, + RichBoolean, + RichBooleanCheckbox, + Select, + Size, + TextInput, + type DateRange +} from '@mtes-mct/monitor-ui' +import styled from 'styled-components' + +import { + LAST_CONTROL_PERIODS_AS_OPTIONS, + LastControlPeriod, + PRIOR_NOTIFICATION_TYPES_AS_OPTIONS, + RECEIVED_AT_PERIODS_AS_OPTIONS, + ReceivedAtPeriod +} from './constants' +import { priorNotificationActions } from '../../slice' + +import type { Promisable } from 'type-fest' + +export type FilterBarProps = { + onQueryChange: (nextQuery: string | undefined) => Promisable + searchQuery: string | undefined +} +export function FilterBar() { + const listFilterValues = useMainAppSelector(store => store.priorNotification.listFilterValues) + + const { fleetSegmentsAsOptions } = useGetFleetSegmentsAsOptions() + const { gearsAsTreeOptions } = useGetGearsAsTreeOptions() + const { portsAsTreeOptions } = useGetPortsAsTreeOptions() + const { speciesAsOptions } = useGetSpeciesAsOptions() + const dispatch = useMainAppDispatch() + + const updateCountryCodes = (nextCountryCodes: string[] | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ countryCodes: nextCountryCodes })) + } + + const updateFleetSegments = (nextFleetSegmentSegments: string[] | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ fleetSegmentSegments: nextFleetSegmentSegments })) + } + + const updateGearCodes = (nextGearCodes: string[] | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ gearCodes: nextGearCodes })) + } + + const updateHasOneOrMoreReportings = (nextHasOneOrMoreReportings: RichBoolean | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ hasOneOrMoreReportings: nextHasOneOrMoreReportings })) + } + + const updateIsLessThanTwelveMetersVessel = (nextIsLessThanTwelveMetersVessel: RichBoolean | undefined) => { + dispatch( + priorNotificationActions.setListFilterValues({ isLessThanTwelveMetersVessel: nextIsLessThanTwelveMetersVessel }) + ) + } + + const updateLastControlPeriod = (nextLastControlPeriod: LastControlPeriod | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ lastControlPeriod: nextLastControlPeriod })) + } + + const updateQuery = (nextQuery: string | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ query: nextQuery })) + } + + const updatePortLocodes = (nextPortLocodes: string[] | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ portLocodes: nextPortLocodes })) + } + + const updateReceivedAtCustomDateRange = (nextReceivedAtCustomDateRange: DateRange | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ receivedAtCustomDateRange: nextReceivedAtCustomDateRange })) + } + + const updateReceivedAtPeriod = (nextReceivedAtPeriod: ReceivedAtPeriod | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ receivedAtPeriod: nextReceivedAtPeriod })) + } + + const updateSpecyCodes = (nextSpecyCodes: string[] | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ specyCodes: nextSpecyCodes })) + } + + const updateTypes = (nextTypes: PriorNotification.PriorNotificationType[] | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ types: nextTypes })) + } + + return ( + + + + + + + + + + + + + + + + + {listFilterValues.receivedAtPeriod === ReceivedAtPeriod.CUSTOM && ( + + + + )} + + ) +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 24px; + + > div:not(:first-child) { + margin-top: 16px; + } +` + +const Row = styled.div` + align-items: center; + display: flex; + + > .Element-Field, + > .Element-Fieldset { + min-width: 200px; + + &:not(:first-child) { + margin-left: 16px; + width: 160px; + } + } + + > .Field-TextInput { + min-width: 280px; + } + + > .Field-MultiCheckbox { + min-width: 320px; + } +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx new file mode 100644 index 0000000000..223f2a174f --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx @@ -0,0 +1,351 @@ +import { customDayjs, THEME, Tag, getOptionsFromLabelledEnum, TableWithSelectableRows } from '@mtes-mct/monitor-ui' +import { capitalizeFirstLetter } from '@utils/capitalizeFirstLetter' + +import { ButtonsGroupRow } from './ButtonsGroupRow' +import { SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' +import { PriorNotification } from '../../PriorNotification.types' + +import type { ColumnDef } from '@tanstack/react-table' + +export const PRIOR_NOTIFICATION_TABLE_COLUMNS: Array> = [ + { + accessorFn: row => row.id, + cell: ({ row }) => ( + + ), + enableSorting: false, + header: ({ table }) => ( + + ), + id: 'select', + size: 50 + }, + { + accessorFn: row => row.estimatedTimeOfArrival, + cell: info => + customDayjs(info.getValue() as string) + .utc() + .format('DD/MM/YYYY à HH[h]mm'), + enableSorting: true, + header: () => 'Arrivée estimée', + id: 'estimatedTimeOfArrival', + size: 120 + }, + { + accessorFn: row => row.scheduledTimeOfLanding, + cell: info => + customDayjs(info.getValue() as string) + .utc() + .format('DD/MM/YYYY à HH[h]mm'), + enableSorting: true, + header: () => 'Débarque prévue', + id: 'scheduledTimeOfLanding', + size: 120 + }, + { + accessorFn: row => `${row.port.name} (${row.port.locode})`, + cell: info => info.getValue(), + enableSorting: true, + header: () => "Port d'arrivée", + id: 'port', + size: 180 + }, + { + accessorFn: row => row.vessel.riskFactor.riskFactor, + cell: info => info.getValue(), + enableSorting: true, + header: () => 'Note', + id: 'riskScore.riskScore', + size: 50 + }, + { + accessorFn: row => row.vessel.vesselName, + cell: info => info.getValue(), + enableSorting: true, + header: () => 'Nom', + id: 'vessel.vesselName', + size: 140 + }, + { + accessorFn: row => row.fleetSegments.map(fleetSegment => fleetSegment.segment).join('/'), + cell: info => info.getValue(), + enableSorting: true, + header: () => 'Segments', + id: 'fleetSegments', + size: 100 + }, + { + accessorFn: row => row.types.map(type => PriorNotification.PRIOR_NOTIFICATION_TYPE_LABEL[type]).join(', '), + cell: info => capitalizeFirstLetter(info.getValue() as string), + enableSorting: true, + header: () => 'Types de préavis', + id: 'types', + size: 140 + }, + { + accessorFn: row => row.alertCount, + cell: info => { + const alertCount = info.getValue() as number + if (!alertCount) { + return null + } + + return ( + {`${ + info.getValue() as number + } sign.`} + ) + }, + enableSorting: false, + header: () => '', + id: 'alertCount', + size: 60 + }, + { + accessorFn: row => row.id, + cell: info => , + enableSorting: false, + header: () => '', + id: 'actions', + size: 56 + } +] + +export const PRIOR_NOTIFICATION_TYPES_AS_OPTIONS = getOptionsFromLabelledEnum( + PriorNotification.PRIOR_NOTIFICATION_TYPE_LABEL +) + +/* eslint-disable sort-keys-fix/sort-keys-fix */ +export const SUB_MENU_LABEL: Record = { + ALL: 'Vue d’ensemble', + MED: 'MED', + MEMN: 'MEMN', + NAMO: 'NAMO', + SA: 'SA', + OUTREMEROA: 'OUTRE-MER OA', + OUTREMEROI: 'OUTRE-MER OI', + EXTRA: 'HORS FAÇADE' +} +export const SUB_MENUS_AS_OPTIONS = getOptionsFromLabelledEnum(SUB_MENU_LABEL) + +export enum LastControlPeriod { + AFTER_ONE_MONTH_AGO = 'AFTER_ONE_MONTH_AGO', + BEFORE_ONE_MONTH_AGO = 'BEFORE_ONE_MONTH_AGO', + BEFORE_ONE_YEAR_AGO = 'BEFORE_ONE_YEAR_AGO', + BEFORE_SIX_MONTHS_AGO = 'BEFORE_SIX_MONTHS_AGO', + BEFORE_THREE_MONTHS_AGO = 'BEFORE_THREE_MONTHS_AGO', + BEFORE_TWO_YEARS_AGO = 'BEFORE_TWO_YEARS_AGO' +} +export const LAST_CONTROL_PERIOD_LABEL: Record = { + AFTER_ONE_MONTH_AGO: 'Contrôlé il y a moins d’1 mois', + BEFORE_ONE_MONTH_AGO: 'Contrôlé il y a plus d’1 mois', + BEFORE_THREE_MONTHS_AGO: 'Contrôlé il y a plus de 3 mois', + BEFORE_SIX_MONTHS_AGO: 'Contrôlé il y a plus de 6 mois', + BEFORE_ONE_YEAR_AGO: 'Contrôlé il y a plus d’1 an', + BEFORE_TWO_YEARS_AGO: 'Contrôlé il y a plus de 2 ans' +} +export const LAST_CONTROL_PERIODS_AS_OPTIONS = getOptionsFromLabelledEnum(LAST_CONTROL_PERIOD_LABEL) + +export enum ReceivedAtPeriod { + AFTER_FOUR_HOURS_AGO = 'AFTER_FOUR_HOURS_AGO', + AFTER_HEIGTH_HOURS_AGO = 'AFTER_HEIGTH_HOURS_AGO', + AFTER_ONE_DAY_AGO = 'AFTER_ONE_DAY_AGO', + AFTER_TWELVE_HOURS_AGO = 'AFTER_TWELVE_HOURS_AGO', + AFTER_TWO_HOURS_AGO = 'AFTER_TWO_HOURS_AGO', + CUSTOM = 'CUSTOM' +} +export const RECEIVED_AT_PERIOD_LABEL: Record = { + AFTER_TWO_HOURS_AGO: 'Arrivée estimée dans moins de 2h', + AFTER_FOUR_HOURS_AGO: 'Arrivée estimée dans moins de 4h', + AFTER_HEIGTH_HOURS_AGO: 'Arrivée estimée dans moins de 8h', + AFTER_TWELVE_HOURS_AGO: 'Arrivée estimée dans moins de 12h', + AFTER_ONE_DAY_AGO: 'Arrivée estimée dans moins de 24h', + CUSTOM: 'Période spécifique' +} +export const RECEIVED_AT_PERIODS_AS_OPTIONS = getOptionsFromLabelledEnum(RECEIVED_AT_PERIOD_LABEL) +/* eslint-enable sort-keys-fix/sort-keys-fix */ + +export const FAKE_PRIOR_NOTIFICATIONS: PriorNotification.PriorNotification[] = [ + { + alertCount: 2, + estimatedTimeOfArrival: '2024-03-01T03:00:00Z', + facade: SeaFrontGroup.MED, + fleetSegments: [ + { + bycatchSpecies: [], + faoAreas: [], + gears: [], + impactRiskFactor: 0.0, + segment: 'SEG01', + segmentName: 'Segment 1', + targetSpecies: [], + year: 2024 + } + ], + id: 1, + isSubmitted: false, + port: { + latitude: 0.0, + locode: 'POR01', + longitude: 0.0, + name: 'Port 1' + }, + reason: PriorNotification.PriorNotificationReason.LANDING, + receivedAt: '2024-03-01T02:00:00Z', + scheduledTimeOfLanding: '2024-03-01T04:00:00Z', + types: [PriorNotification.PriorNotificationType.DEEP_SEA_SPECIES], + vessel: { + beaconNumber: null, + declaredFishingGears: [], + district: '', + districtCode: '', + externalReferenceNumber: 'ABC1234', + flagState: '', + gauge: 0, + imo: '', + internalReferenceNumber: 'FR000123', + ircs: 'ABCDEF', + length: 16.89, + mmsi: '123 456 789', + navigationLicenceExpirationDate: '', + operatorEmails: [], + operatorName: '', + operatorPhones: [], + pinger: true, + power: 0, + proprietorEmails: [], + proprietorName: '', + proprietorPhones: [], + registryPort: '', + riskFactor: { + controlPriorityLevel: 0, + controlRateRiskFactor: 0, + detectabilityRiskFactor: 0, + gearOnboard: undefined, + impactRiskFactor: 0, + lastControlDatetime: '2023-01-05T19:52:00Z', + numberControlsLastFiveYears: 0, + numberControlsLastThreeYears: 0, + numberGearSeizuresLastFiveYears: 0, + numberInfractionsLastFiveYears: 0, + numberSpeciesSeizuresLastFiveYears: 0, + numberVesselSeizuresLastFiveYears: 0, + probabilityRiskFactor: 0, + riskFactor: 3.1, + segmentHighestImpact: '', + segmentHighestPriority: '', + segments: [], + speciesOnboard: undefined + }, + sailingCategory: '', + sailingType: '', + underCharter: false, + vesselEmails: [], + vesselId: 1, + vesselName: 'Vessel 1', + vesselPhones: [], + vesselType: '', + width: 0 + } + }, + { + alertCount: 2, + estimatedTimeOfArrival: '2024-03-01T03:00:00Z', + facade: SeaFrontGroup.NAMO, + fleetSegments: [ + { + bycatchSpecies: [], + faoAreas: [], + gears: [], + impactRiskFactor: 0.0, + segment: 'SEG02', + segmentName: 'Segment 2', + targetSpecies: [], + year: 2024 + }, + { + bycatchSpecies: [], + faoAreas: [], + gears: [], + impactRiskFactor: 0.0, + segment: 'SEG03', + segmentName: 'Segment 3', + targetSpecies: [], + year: 2024 + } + ], + id: 2, + isSubmitted: false, + port: { + latitude: 0.0, + locode: 'POR02', + longitude: 0.0, + name: 'Port 2' + }, + reason: PriorNotification.PriorNotificationReason.LANDING, + receivedAt: '2024-03-01T02:00:00Z', + scheduledTimeOfLanding: '2024-03-01T04:00:00Z', + types: [PriorNotification.PriorNotificationType.DEEP_SEA_SPECIES], + vessel: { + beaconNumber: null, + declaredFishingGears: [], + district: '', + districtCode: '', + externalReferenceNumber: 'DEF5678', + flagState: '', + gauge: 0, + imo: '', + internalReferenceNumber: 'FR000456', + ircs: 'GHIJK', + length: 24.6, + mmsi: '987 654 321', + navigationLicenceExpirationDate: '', + operatorEmails: [], + operatorName: '', + operatorPhones: [], + pinger: true, + power: 0, + proprietorEmails: [], + proprietorName: '', + proprietorPhones: [], + registryPort: '', + riskFactor: { + controlPriorityLevel: 0, + controlRateRiskFactor: 0, + detectabilityRiskFactor: 0, + gearOnboard: undefined, + impactRiskFactor: 0, + lastControlDatetime: '2024-02-18T14:12:00Z', + numberControlsLastFiveYears: 0, + numberControlsLastThreeYears: 0, + numberGearSeizuresLastFiveYears: 0, + numberInfractionsLastFiveYears: 0, + numberSpeciesSeizuresLastFiveYears: 0, + numberVesselSeizuresLastFiveYears: 0, + probabilityRiskFactor: 0, + riskFactor: 2.4, + segmentHighestImpact: '', + segmentHighestPriority: '', + segments: [], + speciesOnboard: undefined + }, + sailingCategory: '', + sailingType: '', + underCharter: false, + vesselEmails: [], + vesselId: 2, + vesselName: 'Vessel 2', + vesselPhones: [], + vesselType: '', + width: 0 + } + } +] diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx new file mode 100644 index 0000000000..1f7360e397 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx @@ -0,0 +1,301 @@ +import { Body } from '@features/SideWindow/components/Body' +import { Header } from '@features/SideWindow/components/Header' +import { Page } from '@features/SideWindow/components/Page' +import { SubMenu } from '@features/SideWindow/SubMenu' +import { useMainAppDispatch } from '@hooks/useMainAppDispatch' +import { useMainAppSelector } from '@hooks/useMainAppSelector' +import { Icon, TableWithSelectableRows, customDayjs } from '@mtes-mct/monitor-ui' +import { + flexRender, + getCoreRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + getExpandedRowModel +} from '@tanstack/react-table' +import { useVirtualizer } from '@tanstack/react-virtual' +import { Fragment, useCallback, useRef, useState } from 'react' +import styled from 'styled-components' + +import { FAKE_PRIOR_NOTIFICATIONS, PRIOR_NOTIFICATION_TABLE_COLUMNS, SUB_MENUS_AS_OPTIONS } from './constants' +import { FilterBar } from './FilterBar' +import { SEA_FRONT_GROUP_SEA_FRONTS, SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' +import { useGetNoticesQuery } from '../../api' +import { PriorNotification } from '../../PriorNotification.types' +import { priorNotificationActions } from '../../slice' + +export function PriorNotificationList() { + // eslint-disable-next-line no-null/no-null + const tableContainerRef = useRef(null) + + const dispatch = useMainAppDispatch() + const selectedSeaFrontGroup = useMainAppSelector(state => state.priorNotification.listFilterValues.seaFrontGroup) + const { data: priorNotifications, isError, isLoading } = useGetNoticesQuery(undefined, { pollingInterval: 60000 }) + + const [rowSelection, setRowSelection] = useState({}) + const [sorting, setSorting] = useState([ + { + desc: true, + id: 'estimatedTimeOfArrival' + } + ]) + + const countNoticesForSeaFrontGroup = useCallback( + (seaFrontGroup: SeaFrontGroup | 'EXTRA'): number => + FAKE_PRIOR_NOTIFICATIONS.filter(({ facade }) => { + if (seaFrontGroup === SeaFrontGroup.ALL) { + return true + } + + return facade && SEA_FRONT_GROUP_SEA_FRONTS[seaFrontGroup] + ? SEA_FRONT_GROUP_SEA_FRONTS[seaFrontGroup].includes(facade as any) + : false + }).length, + [] + ) + + const handleSubMenuChange = useCallback( + (nextSeaFrontGroup: SeaFrontGroup | 'EXTRA') => { + dispatch(priorNotificationActions.setListFilterValues({ seaFrontGroup: nextSeaFrontGroup })) + }, + [dispatch] + ) + + const table = useReactTable({ + columns: PRIOR_NOTIFICATION_TABLE_COLUMNS, + data: FAKE_PRIOR_NOTIFICATIONS, + enableColumnResizing: false, + enableRowSelection: true, + enableSortingRemoval: false, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getRowCanExpand: () => true, + getSortedRowModel: getSortedRowModel(), + onRowSelectionChange: rowId => { + setRowSelection(rowId) + }, + onSortingChange: setSorting, + state: { + rowSelection, + sorting + } + }) + + const { rows } = table.getRowModel() + + const rowVirtualizer = useVirtualizer({ + count: rows.length, + estimateSize: () => 10, + // Pass correct keys to virtualizer it's important when rows change position + getItemKey: useCallback((index: number) => `${rows[index]?.id}`, [rows]), + + getScrollElement: () => tableContainerRef.current, + + overscan: 10 + }) + + const virtualRows = rowVirtualizer.getVirtualItems() + + return ( + <> + + + +

+ Préavis +
+ + + + + + {isError &&
Une erreur est survenue.
} + {isLoading &&
Chargement en cours...
} + {!!priorNotifications && ( + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder ? undefined : ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getCanSort() && + ({ + asc:
, + desc:
+ }[header.column.getIsSorted() as string] ?? )} +
+ )} +
+ ))} + + ))} +
+ + {virtualRows.map(virtualRow => { + const row = rows[virtualRow.index] + if (!row) { + throw new Error('Row not found') + } + + const priorNotification = row.original + + return ( + + + {row?.getVisibleCells().map(cell => ( + row.toggleExpanded()} + style={{ + height: 42, + padding: '0 16px 1px', + verticalAlign: 'middle' + }} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + + {row.getIsExpanded() && ( + + + +

+ PNO reçu : + + {customDayjs(priorNotification.receivedAt).utc().format('DD/MM/YYYY [à] hh[h]mm')} + +

+
+ +

+ Raison du PNO : + + {PriorNotification.PRIOR_NOTIFICATION_REASON_LABEL[priorNotification.reason]} + +

+
+ + + +

+ + {priorNotification.vessel.internalReferenceNumber} (CFR) + + + {priorNotification.vessel.ircs} (Call sign) + + + {priorNotification.vessel.externalReferenceNumber} (Marq. ext.) + + {priorNotification.vessel.mmsi} (MMSI) +

+

+ Taille du navire : + {priorNotification.vessel.length} +

+

+ Dernier contrôle : + + {customDayjs(priorNotification.vessel.riskFactor.lastControlDatetime) + .utc() + .format('[Le] DD/MM/YYYY')} + +

+
+ + Nom du segment : + + {priorNotification.fleetSegments + .map(fleetSegment => fleetSegment.segmentName) + .join(', ')} + + + + Principales espèces à bord soumises à plan : + +
  • Baudroies (NCA) – 280 kg
  • +
  • Merlu européen (HKE) – 140 kg
  • +
  • Bar européen (BSS) – 45 kg
  • +
  • Sole commune (SOL) – 33 kg
  • +
  • Églefin (HAD) – 24 kg
  • +
    +
    + + + + + )} + + ) + })} + + + )} + + + + + ) +} + +const TableWrapper = styled.div` + flex-grow: 1; + width: 1440px; +` + +const ExpandableRow = styled(TableWithSelectableRows.Td)` + cursor: pointer; +` + +const ExpandedRow = TableWithSelectableRows.BodyTr + +const ExpandedRowCell = styled(TableWithSelectableRows.Td).attrs(props => ({ + ...props, + $hasRightBorder: false +}))` + white-space: normal; + + > p:not(:first-child) { + margin-top: 16px; + } +` + +const ExpandedRowLabel = styled.span` + color: ${p => p.theme.color.slateGray}; + display: block; + width: 100%; +` +const ExpandedRowValue = styled.span<{ + $isLight?: boolean +}>` + color: ${p => (p.$isLight ? p.theme.color.slateGray : 'inherit')}; + display: block; +` +const ExpandedRowList = styled.ul` + list-style: none; + padding: 0; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts b/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts new file mode 100644 index 0000000000..50bd7ce165 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts @@ -0,0 +1,25 @@ +import type { LastControlPeriod, ReceivedAtPeriod } from './constants' +import type { SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' +import type { PriorNotification } from '../../PriorNotification.types' +import type { DateRange, RichBoolean, UndefineExcept } from '@mtes-mct/monitor-ui' + +export type ListFilterValues = UndefineExcept< + { + countryCodes: string[] + fleetSegmentSegments: string[] + gearCodes: string[] + hasOneOrMoreReportings: RichBoolean + isLessThanTwelveMetersVessel: RichBoolean + isSent: boolean + isVesselPretargeted: boolean + lastControlPeriod: LastControlPeriod + portLocodes: string[] + query: string + receivedAtCustomDateRange: DateRange + receivedAtPeriod: ReceivedAtPeriod | undefined + seaFrontGroup: SeaFrontGroup | 'EXTRA' + specyCodes: string[] + types: PriorNotification.PriorNotificationType[] + }, + 'receivedAtPeriod' | 'seaFrontGroup' +> diff --git a/frontend/src/features/PriorNotification/slice.ts b/frontend/src/features/PriorNotification/slice.ts new file mode 100644 index 0000000000..1b2c1d6155 --- /dev/null +++ b/frontend/src/features/PriorNotification/slice.ts @@ -0,0 +1,45 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' + +import { ReceivedAtPeriod } from './components/PriorNotificationList/constants' +import { SeaFrontGroup } from '../../domain/entities/seaFront/constants' + +import type { ListFilterValues } from './components/PriorNotificationList/types' + +interface PriorNotificationState { + listFilterValues: ListFilterValues +} +const INITIAL_STATE: PriorNotificationState = { + listFilterValues: { + countryCodes: undefined, + fleetSegmentSegments: undefined, + gearCodes: undefined, + hasOneOrMoreReportings: undefined, + isLessThanTwelveMetersVessel: undefined, + isSent: undefined, + isVesselPretargeted: undefined, + lastControlPeriod: undefined, + portLocodes: undefined, + query: undefined, + receivedAtCustomDateRange: undefined, + receivedAtPeriod: ReceivedAtPeriod.AFTER_FOUR_HOURS_AGO, + seaFrontGroup: SeaFrontGroup.ALL, + specyCodes: undefined, + types: undefined + } +} + +const priorNotificationSlice = createSlice({ + initialState: INITIAL_STATE, + name: 'priorNotification', + reducers: { + setListFilterValues(state, action: PayloadAction>) { + state.listFilterValues = { + ...state.listFilterValues, + ...action.payload + } + } + } +}) + +export const priorNotificationActions = priorNotificationSlice.actions +export const priorNotificationReducer = priorNotificationSlice.reducer diff --git a/frontend/src/features/SideWindow/Menu/index.tsx b/frontend/src/features/SideWindow/Menu/index.tsx index c04d616a75..28911bf664 100644 --- a/frontend/src/features/SideWindow/Menu/index.tsx +++ b/frontend/src/features/SideWindow/Menu/index.tsx @@ -22,6 +22,18 @@ export function Menu({ selectedMenu }: MenuProps) { selected={selectedMenu === SideWindowMenuKey.ALERT_LIST_AND_REPORTING_LIST} title={SideWindowMenuLabel.ALERT_LIST_AND_REPORTING_LIST} /> + {import.meta.env.FRONTEND_PRIOR_NOTIFICATION_LIST_ENABLED === 'true' && ( + dispatch(openSideWindowPath({ menu: SideWindowMenuKey.PRIOR_NOTIFICATION_LIST }))} + role="menuitem" + selected={selectedMenu === SideWindowMenuKey.PRIOR_NOTIFICATION_LIST} + title={SideWindowMenuLabel.PRIOR_NOTIFICATION_LIST} + /> + )} ({ counter, onChange, options, const Wrapper = styled.div` min-width: 222px; + user-select: none; + + * { + user-select: none; + } ` const Menu = styled.div` diff --git a/frontend/src/features/SideWindow/components/Body.tsx b/frontend/src/features/SideWindow/components/Body.tsx new file mode 100644 index 0000000000..007fc5f566 --- /dev/null +++ b/frontend/src/features/SideWindow/components/Body.tsx @@ -0,0 +1,8 @@ +import styled from 'styled-components' + +export const Body = styled.div` + display: flex; + flex-grow: 1; + padding: 32px; + flex-direction: column; +` diff --git a/frontend/src/features/SideWindow/components/Header.tsx b/frontend/src/features/SideWindow/components/Header.tsx new file mode 100644 index 0000000000..666748a8a6 --- /dev/null +++ b/frontend/src/features/SideWindow/components/Header.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components' + +const RawHeader = styled.div` + align-items: center; + background-color: ${p => p.theme.color.white}; + border-bottom: solid 2px ${p => p.theme.color.gainsboro}; + display: flex; + justify-content: space-between; + min-height: 80px; + padding: 0 32px; +` + +const Title = styled.h1` + color: ${p => p.theme.color.charcoal}; + font-size: 22px; + font-weight: 700; + line-height: 1.4; +` + +export const Header = Object.assign(RawHeader, { + Title +}) diff --git a/frontend/src/features/SideWindow/components/Page.tsx b/frontend/src/features/SideWindow/components/Page.tsx new file mode 100644 index 0000000000..88f51962e1 --- /dev/null +++ b/frontend/src/features/SideWindow/components/Page.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components' + +import { NoRsuiteOverrideWrapper } from '../../../ui/NoRsuiteOverrideWrapper' + +export const Page = styled(NoRsuiteOverrideWrapper)` + display: flex; + flex-direction: column; + flex-grow: 1; + margin-bottom: 20px; + overflow: auto; +` diff --git a/frontend/src/features/SideWindow/index.tsx b/frontend/src/features/SideWindow/index.tsx index bf35600ea9..03d4ae8ead 100644 --- a/frontend/src/features/SideWindow/index.tsx +++ b/frontend/src/features/SideWindow/index.tsx @@ -32,6 +32,7 @@ import { useMainAppSelector } from '../../hooks/useMainAppSelector' import { FrontendErrorBoundary } from '../../ui/FrontendErrorBoundary' import { Loader as MissionFormLoader } from '../Mission/components/MissionForm/Loader' import { MissionList } from '../Mission/components/MissionList' +import { PriorNotificationList } from '../PriorNotification/components/PriorNotificationList' import { setEditedReportingInSideWindow } from '../Reporting/slice' import { getAllCurrentReportings } from '../Reporting/useCases/getAllCurrentReportings' @@ -90,7 +91,7 @@ export function SideWindow({ isFromURL }: SideWindowProps) { }, []) useEffect(() => { - if (editedReportingInSideWindow || openedBeaconMalfunctionInKanban) { + if (editedReportingInSideWindow ?? openedBeaconMalfunctionInKanban) { setIsOverlayed(true) return @@ -108,8 +109,15 @@ export function SideWindow({ isFromURL }: SideWindowProps) { dispatch(getInfractions()) dispatch(getAllGearCodes()) - dispatch(openSideWindowPath({ menu: SideWindowMenuKey.ALERT_LIST_AND_REPORTING_LIST })) + dispatch(openSideWindowPath({ menu: SideWindowMenuKey.PRIOR_NOTIFICATION_LIST })) } + + dispatch(getOperationalAlerts()) + dispatch(getAllBeaconMalfunctions()) + dispatch(getSilencedAlerts()) + dispatch(getAllCurrentReportings()) + dispatch(getInfractions()) + dispatch(getAllGearCodes()) }, [dispatch, isFromURL]) useEffect(() => { @@ -117,7 +125,7 @@ export function SideWindow({ isFromURL }: SideWindowProps) { }, []) return ( - + {!isFirstRender && ( @@ -145,6 +153,7 @@ export function SideWindow({ isFromURL }: SideWindowProps) { } /> )} {selectedPath.menu === SideWindowMenuKey.BEACON_MALFUNCTION_BOARD && } + {selectedPath.menu === SideWindowMenuKey.PRIOR_NOTIFICATION_LIST && } {selectedPath.menu === SideWindowMenuKey.MISSION_LIST && } {selectedPath.menu === SideWindowMenuKey.MISSION_FORM && ( diff --git a/frontend/src/features/Station/slice.ts b/frontend/src/features/Station/slice.ts index 69baaffbff..bb2e7525b7 100644 --- a/frontend/src/features/Station/slice.ts +++ b/frontend/src/features/Station/slice.ts @@ -1,10 +1,10 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit' -interface MainWindowState { +interface StationState { highlightedStationIds: number[] selectedStationId: number | undefined } -const INITIAL_STATE: MainWindowState = { +const INITIAL_STATE: StationState = { highlightedStationIds: [], selectedStationId: undefined } diff --git a/frontend/src/features/VesselList/VesselListFilters.tsx b/frontend/src/features/VesselList/VesselListFilters.tsx index 7a1138e0e0..8e20ce359a 100644 --- a/frontend/src/features/VesselList/VesselListFilters.tsx +++ b/frontend/src/features/VesselList/VesselListFilters.tsx @@ -1,10 +1,9 @@ -import Countries from 'i18n-iso-countries' -import COUNTRIES_FR from 'i18n-iso-countries/langs/fr.json' import React, { useCallback, useMemo, useState } from 'react' import { Checkbox, CheckboxGroup, MultiCascader, SelectPicker, Tag, TagPicker } from 'rsuite' import styled from 'styled-components' import { lastControlAfterLabels, lastPositionTimeAgoLabels } from './dataFormatting' +import { COUNTRIES_AS_OPTIONS } from '../../constants' import { COLORS } from '../../constants/constants' import { LayerType as LayersType } from '../../domain/entities/layers/constants' import { VesselLocation, vesselSize } from '../../domain/entities/vessel/vessel' @@ -14,13 +13,6 @@ import PolygonFilterSVG from '../icons/Filtre_zone_polygone.svg?react' import BoxFilterSVG from '../icons/Filtre_zone_rectangle.svg?react' import FilterTag from '../MapButtons/VesselFilters/FilterTag' -Countries.registerLocale(COUNTRIES_FR) - -const countriesField = Object.keys(Countries.getAlpha2Codes()).map(country => ({ - label: Countries.getName(country, 'fr'), - value: country.toLowerCase() -})) - function renderTagPickerMenuItem(item) { return } @@ -146,7 +138,7 @@ function UnmemoizedVesselListFilters({ /> { + if (!fleetSegments) { + return undefined + } + + return fleetSegments + .map(({ segment, segmentName }) => ({ + label: `${segment} – ${String(segmentName)}`, + value: segment + })) + .sort((a, b) => a.label.localeCompare(b.label)) + }, + + // Fleet segments are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + fleetSegmentsAsOptions, + isLoading + } +} diff --git a/frontend/src/hooks/useGetGearsAsTreeOptions.ts b/frontend/src/hooks/useGetGearsAsTreeOptions.ts new file mode 100644 index 0000000000..eab888eebb --- /dev/null +++ b/frontend/src/hooks/useGetGearsAsTreeOptions.ts @@ -0,0 +1,43 @@ +import { useGetGearsQuery } from '@api/gear' +import { uniq } from 'lodash' +import { useMemo } from 'react' + +import type { TreeOption } from '@mtes-mct/monitor-ui' + +/** + * Fetches gears and returns them as tree options with their `code` property as option value. + */ +export function useGetGearsAsTreeOptions() { + const { data: gears, error, isLoading } = useGetGearsQuery() + + const gearsAsTreeOptions: TreeOption[] | undefined = useMemo( + () => { + if (!gears) { + return undefined + } + + const sortedGearCategories = uniq(gears.map(gear => gear.category)).sort() + + return sortedGearCategories.map(category => ({ + children: gears + .filter(gear => gear.category === category) + .map(gear => ({ + label: `${gear.name} – ${gear.code}`, + value: gear.code + })) + .sort((a, b) => a.label.localeCompare(b.label)), + label: category + })) + }, + + // Gears are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + gearsAsTreeOptions, + isLoading + } +} diff --git a/frontend/src/hooks/useGetPortsAsTreeOptions.ts b/frontend/src/hooks/useGetPortsAsTreeOptions.ts new file mode 100644 index 0000000000..11dcab89e3 --- /dev/null +++ b/frontend/src/hooks/useGetPortsAsTreeOptions.ts @@ -0,0 +1,43 @@ +import { useGetPortsQuery } from '@api/port' +import { useMemo } from 'react' + +import type { TreeOption } from '@mtes-mct/monitor-ui' + +/** + * Fetches ports and returns them as tree options with their `locode` property as option value. + */ +export function useGetPortsAsTreeOptions() { + const { data: ports, error, isLoading } = useGetPortsQuery() + + const portsAsTreeOptions: TreeOption[] | undefined = useMemo( + () => { + if (!ports) { + return undefined + } + + // TODO Add the department to the ports list API endpoint data. + const sortedPortCategories = ['Un département'] + + return sortedPortCategories.map(category => ({ + children: ports + // .filter(port => port.department.code === category) + .map(port => ({ + label: port.name, + value: port.locode + })) + .sort((a, b) => a.label.localeCompare(b.label)), + label: category + })) + }, + + // Ports are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + isLoading, + portsAsTreeOptions + } +} diff --git a/frontend/src/hooks/useGetSpeciesAsOptions.ts b/frontend/src/hooks/useGetSpeciesAsOptions.ts new file mode 100644 index 0000000000..d5f8644c3f --- /dev/null +++ b/frontend/src/hooks/useGetSpeciesAsOptions.ts @@ -0,0 +1,36 @@ +import { useGetSpeciesQuery } from '@api/specy' +import { useMemo } from 'react' + +import type { Option } from '@mtes-mct/monitor-ui' + +/** + * Fetches species and returns them as options with their `code` property as option value. + */ +export function useGetSpeciesAsOptions() { + const { data: speciesAndGroups, error, isLoading } = useGetSpeciesQuery() + + const speciesAsOptions: Option[] | undefined = useMemo( + () => { + if (!speciesAndGroups) { + return undefined + } + + return speciesAndGroups.species + .map(({ code, name }) => ({ + label: `${name} (${code})`, + value: code + })) + .sort((a, b) => a.label.localeCompare(b.label)) + }, + + // Species are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + isLoading, + speciesAsOptions + } +} diff --git a/frontend/src/hooks/useTable/types.ts b/frontend/src/hooks/useTable/types.ts index eb351c28f7..019b74ff11 100644 --- a/frontend/src/hooks/useTable/types.ts +++ b/frontend/src/hooks/useTable/types.ts @@ -1,4 +1,5 @@ -import type { CollectionItem, Native } from '../../types' +import type { CollectionItem } from '../../types' +import type { Native } from '@mtes-mct/monitor-ui' import type { IFuseOptions } from 'fuse.js' export type TableColumn = Record> = { diff --git a/frontend/src/store/reducers.ts b/frontend/src/store/reducers.ts index bfa16c599a..8cb6fa6c1a 100644 --- a/frontend/src/store/reducers.ts +++ b/frontend/src/store/reducers.ts @@ -27,6 +27,7 @@ import { customZoneReducer, type CustomZoneState } from '../features/CustomZone/ import { logbookReducer } from '../features/Logbook/slice' import { missionFormReducer } from '../features/Mission/components/MissionForm/slice' import { missionListReducer, type MissionListState } from '../features/Mission/components/MissionList/slice' +import { priorNotificationReducer } from '../features/PriorNotification/slice' import { regulatoryLayerSearchReducer } from '../features/Regulation/components/RegulationSearch/slice' import { regulatoryReducer } from '../features/Regulation/slice' import { reportingReducer } from '../features/Reporting/slice' @@ -67,6 +68,7 @@ export const mainReducer = { beaconMalfunction: beaconMalfunctionReducer, // TODO Pass that to singular. controls: controlReducer, + controlUnitDialog: controlUnitDialogReducer, controlUnitListDialog: controlUnitListDialogPersistedReducer, customZone: persistReducerTyped( @@ -90,6 +92,7 @@ export const mainReducer = { }, missionListReducer ), + priorNotification: priorNotificationReducer, regulatoryLayerSearch: regulatoryLayerSearchReducer, reporting: reportingReducer, sideWindow: sideWindowReducer, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 6a47a9c699..87f6c78208 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,3 +1,4 @@ +import type { Native } from '@mtes-mct/monitor-ui' import type { ConditionalKeys, Exact } from 'type-fest' // ============================================================================= @@ -10,11 +11,6 @@ export type CollectionItem = { export type FormikFormError = Record | undefined -export type Native = boolean | null | number | string | undefined -export type NativeAny = boolean | NativeArray | NativeObject | null | number | string | undefined -export type NativeArray = Array -export type NativeObject = { [x: string]: NativeAny } | {} - export type MenuItem = { code: T name: string @@ -23,12 +19,6 @@ export type MenuItem = { // ============================================================================= // UTILITIES -export type DeepPartial = T extends object - ? { - [P in keyof T]?: DeepPartial - } - : T - export type PartialExcept, RequiredKeys extends keyof T> = Partial< Omit > & diff --git a/frontend/src/ui/NoRsuiteOverrideWrapper.tsx b/frontend/src/ui/NoRsuiteOverrideWrapper.tsx index f4fb3bf12c..2848ab9e23 100644 --- a/frontend/src/ui/NoRsuiteOverrideWrapper.tsx +++ b/frontend/src/ui/NoRsuiteOverrideWrapper.tsx @@ -27,15 +27,9 @@ import styled from 'styled-components' // TODO Fix these CSS issues. export const NoRsuiteOverrideWrapper = styled.div` box-sizing: border-box; - /* - This is the closest to as round 18px we can get because 13px can't produce round line heights (other than 1), - leading to regular cascaded height issues, especially on HD screens (2 or more real pixels per theoric pixel) - */ - line-height: 1.3846; - * { - box-sizing: border-box; - line-height: 1.3846; + .Field-Checkbox * { + line-height: 18px; } h1, diff --git a/frontend/src/utils/nullify.ts b/frontend/src/utils/nullify.ts index 62c438698e..17a19c7fd6 100644 --- a/frontend/src/utils/nullify.ts +++ b/frontend/src/utils/nullify.ts @@ -6,7 +6,7 @@ import { isArray } from './isArray' import { isObject } from './isObject' import { FrontendError } from '../libs/FrontendError' -import type { NativeAny, NativeArray, NativeObject } from '../types' +import type { NativeAny, NativeArray, NativeObject } from '@mtes-mct/monitor-ui' type Nullify = T extends undefined ? null diff --git a/frontend/src/utils/undefinedize.ts b/frontend/src/utils/undefinedize.ts index aff5503a5b..232a71622a 100644 --- a/frontend/src/utils/undefinedize.ts +++ b/frontend/src/utils/undefinedize.ts @@ -6,7 +6,7 @@ import { isArray } from './isArray' import { isObject } from './isObject' import { FrontendError } from '../libs/FrontendError' -import type { NativeAny, NativeArray, NativeObject } from '../types' +import type { NativeAny, NativeArray, NativeObject } from '@mtes-mct/monitor-ui' type Undefinedized = T extends null ? undefined diff --git a/infra/docker/docker-compose.cypress.yml b/infra/docker/docker-compose.cypress.yml index fe4981d39a..15aa657751 100644 --- a/infra/docker/docker-compose.cypress.yml +++ b/infra/docker/docker-compose.cypress.yml @@ -46,6 +46,7 @@ services: - FRONTEND_SHOM_KEY=rg8ele7cft4ujkwjspsmtwas - FRONTEND_MISSION_FORM_AUTO_SAVE_ENABLED=true # Even if we inject this env var, the value is not used (see cypress.config.ts) - FRONTEND_MISSION_FORM_AUTO_UPDATE_ENABLED=true + - FRONTEND_PRIOR_NOTIFICATION_LIST_ENABLED=true ports: - 8880:8880 - 8000:8000 diff --git a/infra/docker/docker-compose.puppeteer.yml b/infra/docker/docker-compose.puppeteer.yml index 78726430ae..f3883e824a 100644 --- a/infra/docker/docker-compose.puppeteer.yml +++ b/infra/docker/docker-compose.puppeteer.yml @@ -62,6 +62,7 @@ services: - FRONTEND_SHOM_KEY=rg8ele7cft4ujkwjspsmtwas - FRONTEND_MISSION_FORM_AUTO_SAVE_ENABLED=true - FRONTEND_MISSION_FORM_AUTO_UPDATE_ENABLED=true + - FRONTEND_PRIOR_NOTIFICATION_LIST_ENABLED=true ports: - 8880:8880 - 8000:8000 From 5344560b2259a9be1a035e4a500c68957d81b56a Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 5 Mar 2024 09:46:59 +0100 Subject: [PATCH 22/82] Fix tests --- .../src/main/resources/db/testdata/V666.5__Insert_logbook.sql | 4 ++-- .../database/repositories/JpaLogbookReportRepositoryITests.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql index 50888bc7f3..932a48df6f 100644 --- a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql +++ b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql @@ -449,8 +449,8 @@ VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-10-20T12:16:00Z"}', '2021-01-18T07:17:26.736456Z', 'ERS', 'TurboCatch (3.7-1)'), - ('OOF20191011059902', 9463715, 'OOF', '2019-10-11T08:16:00Z', 'DAT', 'OOF20191011059902', null, - '2019-10-21T08:16:00Z', + ('OOF20191011059902', 9463715, 'OOF', '2019-10-21T08:16:00Z', 'DAT', 'OOF20191011059902', null, + '2019-10-11T08:16:00Z', 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', '2021-01-18T07:17:19.04244Z', 'ERS', 'TurboCatch (3.7-1)'), diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt index 31c83973f0..47bd8b3429 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt @@ -237,7 +237,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { assertThat(pnoMessage.catchOnboard.first().effortZone).isEqualTo("C") assertThat(pnoMessage.catchOnboard.first().economicZone).isEqualTo("FRA") assertThat(pnoMessage.catchOnboard.first().statisticalRectangle).isEqualTo("23E6") - assertThat(pnoMessage.tripStartDate.toString()).isEqualTo("2019-10-11T00:00Z") + assertThat(pnoMessage.tripStartDate.toString()).isEqualTo("2024-03-02T00:00Z") assertThat(pnoMessage.predictedArrivalDateTime).isAfter(ZonedDateTime.now().minusDays(5)) // EOF From b7fafcd4eb45eef2c158f2d2dc3fd58f1ec2c16d Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 11 Mar 2024 10:43:50 +0100 Subject: [PATCH 23/82] Extract catch_to_land from PNOs --- .../pipeline/parsers/ers/childless_parsers.py | 9 ++++++--- .../src/pipeline/parsers/ers/log_parsers.py | 8 ++++++-- .../src/pipeline/parsers/flux/log_parsers.py | 20 +++++++++---------- .../test_pipeline/test_parsers/test_ers.py | 1 + .../test_pipeline/test_parsers/test_flux.py | 6 ++++++ 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/datascience/src/pipeline/parsers/ers/childless_parsers.py b/datascience/src/pipeline/parsers/ers/childless_parsers.py index 9d4562c498..5108d331c4 100644 --- a/datascience/src/pipeline/parsers/ers/childless_parsers.py +++ b/datascience/src/pipeline/parsers/ers/childless_parsers.py @@ -34,11 +34,14 @@ def parse_pro(pro): return data -def parse_spe(spe): +def parse_spe(spe, catch_to_land: bool = False): + weight_attribute = "WL" if catch_to_land else "WT" + number_of_fish_attribute = "FL" if catch_to_land else "NF" + data = { "species": spe.get("SN"), - "weight": try_float(spe.get("WT")), - "nbFish": try_float(spe.get("NF")), + "weight": try_float(spe.get(weight_attribute)), + "nbFish": try_float(spe.get(number_of_fish_attribute)), } children = tagged_children(spe) diff --git a/datascience/src/pipeline/parsers/ers/log_parsers.py b/datascience/src/pipeline/parsers/ers/log_parsers.py index c810a02dcd..5abcbe40f3 100644 --- a/datascience/src/pipeline/parsers/ers/log_parsers.py +++ b/datascience/src/pipeline/parsers/ers/log_parsers.py @@ -243,8 +243,12 @@ def parse_pno(pno): value = {**value, **ras_data} if "SPE" in children: - catches = [parse_spe(spe) for spe in children["SPE"]] - value["catchOnboard"] = catches + value["catchOnboard"] = [ + parse_spe(spe, catch_to_land=False) for spe in children["SPE"] + ] + value["catchToLand"] = [ + parse_spe(spe, catch_to_land=True) for spe in children["SPE"] + ] if "POS" in children: assert len(children["POS"]) == 1 diff --git a/datascience/src/pipeline/parsers/flux/log_parsers.py b/datascience/src/pipeline/parsers/flux/log_parsers.py index b9133b626b..e5eeea6c1a 100644 --- a/datascience/src/pipeline/parsers/flux/log_parsers.py +++ b/datascience/src/pipeline/parsers/flux/log_parsers.py @@ -15,7 +15,6 @@ def null_parser(el: xml.etree.ElementTree.Element): def parse_dep(dep): - value = { "departureDatetimeUtc": get_text(dep, ".//ram:OccurrenceDateTime/udt:DateTime"), "departurePort": get_text( @@ -56,7 +55,6 @@ def parse_dep(dep): def parse_far(far): - value = {"farDatetimeUtc": get_text(far, ".//ram:OccurrenceDateTime/udt:DateTime")} children = tagged_children(far) @@ -99,7 +97,6 @@ def parse_far(far): def parse_dis(dis): - value = { "discardDatetimeUtc": get_text(dis, ".//ram:OccurrenceDateTime/udt:DateTime") } @@ -126,7 +123,6 @@ def parse_dis(dis): def parse_coe(coe): - children = tagged_children(coe) value = { @@ -183,7 +179,6 @@ def parse_cox(cox): def parse_pno(pno): - children = tagged_children(pno) value = { @@ -208,13 +203,19 @@ def parse_pno(pno): zone_data = complete_ras(zone_data) if "SpecifiedFACatch" in children: - unloaded_catches = pno.findall( + catch_onboard = pno.findall( + ".//ram:SpecifiedFACatch[ram:TypeCode='ONBOARD']", NS_FLUX + ) + catch_to_land = pno.findall( ".//ram:SpecifiedFACatch[ram:TypeCode='UNLOADED']", NS_FLUX ) - catches = [parse_spe(spe) for spe in unloaded_catches] + catch_onboard = [parse_spe(spe) for spe in catch_onboard] + catch_to_land = [parse_spe(spe) for spe in catch_to_land] if hasRelatedFLUXLocation: - catches = [dict(item, **zone_data) for item in catches] - value["catchOnboard"] = catches + catch_to_land = [dict(item, **zone_data) for item in catch_to_land] + catch_onboard = [dict(item, **zone_data) for item in catch_onboard] + value["catchOnboard"] = catch_onboard + value["catchToLand"] = catch_to_land pos = get_element(pno, ".//ram:SpecifiedPhysicalFLUXGeographicalCoordinate") if pos is not None: @@ -225,7 +226,6 @@ def parse_pno(pno): def parse_lan(lan): - value = { "landingDatetimeUtc": get_text(lan, ".//ram:EndDateTime/udt:DateTime"), "port": get_text( diff --git a/datascience/tests/test_pipeline/test_parsers/test_ers.py b/datascience/tests/test_pipeline/test_parsers/test_ers.py index b7fd2dadbe..6c86410cd8 100644 --- a/datascience/tests/test_pipeline/test_parsers/test_ers.py +++ b/datascience/tests/test_pipeline/test_parsers/test_ers.py @@ -302,6 +302,7 @@ def test_pno_parser(): value = data["value"] assert set(value) == { "catchOnboard", + "catchToLand", "predictedArrivalDatetimeUtc", "predictedLandingDatetimeUtc", "tripStartDate", diff --git a/datascience/tests/test_pipeline/test_parsers/test_flux.py b/datascience/tests/test_pipeline/test_parsers/test_flux.py index e909852df6..be12c4d8f7 100644 --- a/datascience/tests/test_pipeline/test_parsers/test_flux.py +++ b/datascience/tests/test_pipeline/test_parsers/test_flux.py @@ -479,6 +479,9 @@ def test_batch_parse(): "catchOnboard": [ {"species": "GHL", "weight": 1500.0, "nbFish": None} ], + "catchToLand": [ + {"species": "GHL", "weight": 1500.0, "nbFish": None} + ], }, "SRC-TRP-TTT20200506194103340", ], @@ -504,6 +507,9 @@ def test_batch_parse(): "catchOnboard": [ {"species": "GHL", "weight": 1500.0, "nbFish": None} ], + "catchToLand": [ + {"species": "GHL", "weight": 1500.0, "nbFish": None} + ], }, "SRC-TRP-TTT20200506194103340", ], From 4a274ea8adaba77c0f9276a3fbe7b22cf7d70b1f Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 11 Mar 2024 14:31:24 +0100 Subject: [PATCH 24/82] Add catch_to_land in backend --- .../domain/entities/logbook/messages/PNO.kt | 1 + .../resources/db/testdata/V666.5__Insert_logbook.sql | 2 +- .../repositories/JpaLogbookReportRepositoryITests.kt | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt index 4855a4bd9b..3cf3113d75 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt @@ -15,6 +15,7 @@ class PNO() : LogbookMessageValue { var port: String? = null var portName: String? = null var catchOnboard: List = listOf() + var catchToLand: List = listOf() @JsonProperty("predictedArrivalDatetimeUtc") var predictedArrivalDateTime: ZonedDateTime? = null diff --git a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql index 932a48df6f..576a6b47be 100644 --- a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql +++ b/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql @@ -452,7 +452,7 @@ VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF ('OOF20191011059902', 9463715, 'OOF', '2019-10-21T08:16:00Z', 'DAT', 'OOF20191011059902', null, '2019-10-11T08:16:00Z', 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', - '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', + '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "catchToLand": [{"weight": 15.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 151.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', '2021-01-18T07:17:19.04244Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22113048321388', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF20191011059902', '2019-10-30T11:32:00Z', null, null, null, null, null, null, '', diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt index 47bd8b3429..a439cf6289 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt @@ -9,6 +9,7 @@ import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound import fr.gouv.cnsp.monitorfish.domain.use_cases.TestUtils import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable +import org.hibernate.query.sqm.TemporalUnit import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -237,7 +238,15 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { assertThat(pnoMessage.catchOnboard.first().effortZone).isEqualTo("C") assertThat(pnoMessage.catchOnboard.first().economicZone).isEqualTo("FRA") assertThat(pnoMessage.catchOnboard.first().statisticalRectangle).isEqualTo("23E6") - assertThat(pnoMessage.tripStartDate.toString()).isEqualTo("2024-03-02T00:00Z") + assertThat(pnoMessage.catchToLand).hasSize(4) + assertThat(pnoMessage.catchToLand.first().weight).isEqualTo(15.0) + assertThat(pnoMessage.catchToLand.first().numberFish).isEqualTo(null) + assertThat(pnoMessage.catchToLand.first().species).isEqualTo("SLS") + assertThat(pnoMessage.catchToLand.first().faoZone).isEqualTo("27.8.a") + assertThat(pnoMessage.catchToLand.first().effortZone).isEqualTo("C") + assertThat(pnoMessage.catchToLand.first().economicZone).isEqualTo("FRA") + assertThat(pnoMessage.catchToLand.first().statisticalRectangle).isEqualTo("23E6") + assertThat(pnoMessage.tripStartDate).isAfter(ZonedDateTime.now().minusDays(5)) assertThat(pnoMessage.predictedArrivalDateTime).isAfter(ZonedDateTime.now().minusDays(5)) // EOF From 717b7455408a6315eb2f3b5aa0e58a1af1e0b7bc Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 11 Mar 2024 14:58:20 +0100 Subject: [PATCH 25/82] Move pno_types info into field --- .../V0.245__Update_logbook_reports_table.sql | 1 - datascience/src/pipeline/flows/enrich_logbook.py | 13 +++++++++---- .../test_pipeline/test_flows/test_enrich_logbook.py | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql index c499e85db7..c5fda1fddb 100644 --- a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql +++ b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql @@ -1,5 +1,4 @@ ALTER TABLE public.logbook_reports ADD COLUMN enriched BOOLEAN NOT NULL DEFAULT false, ADD COLUMN trip_gears jsonb, - ADD COLUMN pno_types jsonb, ADD COLUMN trip_segments jsonb; diff --git a/datascience/src/pipeline/flows/enrich_logbook.py b/datascience/src/pipeline/flows/enrich_logbook.py index 6160b80bff..04d2176cd6 100644 --- a/datascience/src/pipeline/flows/enrich_logbook.py +++ b/datascience/src/pipeline/flows/enrich_logbook.py @@ -42,7 +42,7 @@ def reset_pnos(period: Period): "SET " " enriched = false," " trip_gears = NULL," - " pno_types = NULL," + " value = value - 'pnoTypes'," " trip_segments = NULL " "WHERE p.operation_datetime_utc >= :start " "AND p.operation_datetime_utc <= :end " @@ -366,9 +366,14 @@ def load_enriched_pnos(enriched_pnos: pd.DataFrame, period: Period, logger: Logg " trip_gears = CASE " " WHEN ep.trip_gears = 'null' THEN '[]'::jsonb " " ELSE ep.trip_gears END, " - " pno_types = CASE " - " WHEN ep.pno_types = 'null' THEN '[]'::jsonb " - " ELSE ep.pno_types END, " + " value = jsonb_set(" + " value, " + " '{pnoTypes}', " + " CASE " + " WHEN ep.pno_types = 'null' THEN '[]'::jsonb " + " ELSE ep.pno_types " + " END" + " ), " " trip_segments = CASE " " WHEN ep.trip_segments = 'null' THEN '[]'::jsonb " " ELSE ep.trip_segments END " diff --git a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py index 9fc4d8e787..f139aeda6b 100644 --- a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py +++ b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py @@ -594,7 +594,7 @@ def test_merge_segments_and_types( def test_load_then_reset_logbook(reset_test_data, pnos_to_load, expected_loaded_pnos): query = ( - "SELECT id, enriched, trip_gears, pno_types, trip_segments " + "SELECT id, enriched, trip_gears, value->'pnoTypes' AS pno_types, trip_segments " "FROM logbook_reports WHERE log_type = 'PNO' ORDER BY id" ) initial_pnos = read_query(query, db="monitorfish_remote") @@ -620,7 +620,7 @@ def test_load_then_reset_logbook(reset_test_data, pnos_to_load, expected_loaded_ def test_flow(reset_test_data): query = ( - "SELECT id, enriched, trip_gears, pno_types, trip_segments " + "SELECT id, enriched, trip_gears, value->'pnoTypes' AS pno_types, trip_segments " "FROM logbook_reports WHERE log_type = 'PNO' ORDER BY id" ) From 80c4e57401f930b823ffb433e444c75522b3b7f3 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Mon, 11 Mar 2024 19:31:29 +0100 Subject: [PATCH 26/82] Add test data seed generator --- Makefile | 1 + frontend/.eslintrc.js | 11 +- frontend/package-lock.json | 33 ++++- frontend/package.json | 1 + frontend/scripts/generate_test_data_seeds.mjs | 121 ++++++++++++++++++ 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 frontend/scripts/generate_test_data_seeds.mjs diff --git a/Makefile b/Makefile index 9b621218cf..62481bfd21 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ run-front: run-back: run-stubbed-apis docker compose up -d --quiet-pull --wait db + cd frontend && node ./scripts/generate_test_data_seeds.mjs cd backend && ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' run-back-with-monitorenv: run-monitorenv diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 8f20556e0f..0194ea7159 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -8,14 +8,14 @@ module.exports = { ecmaVersion: 2022, project: path.join(__dirname, 'tsconfig.json') }, - ignorePatterns: ['.eslintrc.js', '.eslintrc.partial.js', 'scripts/*'], + ignorePatterns: ['.eslintrc.js', '.eslintrc.partial.js'], env: { browser: true }, rules: { curly: ['error', 'all'], 'newline-before-return': 'error', - 'no-console': 'error', + 'no-console': ['error', { allow: ['info', 'warn', 'error'] }], 'import/no-default-export': 'error', 'import/order': [ @@ -104,6 +104,13 @@ module.exports = { } ], '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], '@typescript-eslint/prefer-nullish-coalescing': 'warn', 'typescript-sort-keys/interface': 'error', diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bbe9e4f3c9..ce706b9af0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -113,6 +113,7 @@ "lint-staged": "14.0.1", "prettier": "3.0.0", "puppeteer": "21.7.0", + "strip-json-comments": "5.0.1", "ts-jest": "29.1.2", "type-fest": "4.0.0", "typescript": "5.2.2", @@ -1131,6 +1132,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -9865,6 +9878,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-config/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -14962,12 +14987,12 @@ } }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", + "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/frontend/package.json b/frontend/package.json index 504d69011c..90a34c45ee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -137,6 +137,7 @@ "lint-staged": "14.0.1", "prettier": "3.0.0", "puppeteer": "21.7.0", + "strip-json-comments": "5.0.1", "ts-jest": "29.1.2", "type-fest": "4.0.0", "typescript": "5.2.2", diff --git a/frontend/scripts/generate_test_data_seeds.mjs b/frontend/scripts/generate_test_data_seeds.mjs new file mode 100644 index 0000000000..e0664b4ca9 --- /dev/null +++ b/frontend/scripts/generate_test_data_seeds.mjs @@ -0,0 +1,121 @@ +/* eslint-disable import/no-extraneous-dependencies, no-await-in-loop, no-restricted-syntax */ + +import { promises as fs } from 'fs' +import { join } from 'path' +import stripJsonComments from 'strip-json-comments' + +const DIRECTORY_PATH = join(import.meta.url, '../../../backend/src/main/resources/db/testdata').replace('file:', '') + +function setJsonbSqlPropsToNull(obj) { + const processObject = currentObj => { + const processedObj = Array.isArray(currentObj) ? [] : {} + Object.entries(currentObj).forEach(([key, value]) => { + if (Array.isArray(value)) { + processedObj[key] = value.map(valueItem => processObject(valueItem)) + } else if (typeof value === 'object' && value !== null) { + processedObj[key] = processObject(value) + } else { + // Temporarily set :jsonb > :sql properties to null in the `INSERT` statement + // so that they can be updated with subsequent `UPDATE` statements + processedObj[key] = key.endsWith(':sql') ? null : value + } + }) + + return processedObj + } + + return `'${JSON.stringify(processObject(obj)).replace(/:sql"/g, '"').replace(/'/g, "''")}'` +} + +function generateInsertStatement(row, table) { + const sqlColumns = [] + const sqlValues = [] + + const rowAsKeyValuePairs = Object.entries(row) + + for (const [key, value] of rowAsKeyValuePairs) { + const sqlColumn = key.replace(/:(jsonb|sql)$/, '') + if (key.endsWith(':jsonb')) { + const sqlValue = setJsonbSqlPropsToNull(value) + + sqlColumns.push(sqlColumn) + sqlValues.push(sqlValue) + } else { + const processedValue = + // eslint-disable-next-line no-nested-ternary + value === null + ? 'null' + : typeof value !== 'string' || key.endsWith(':sql') + ? value + : `'${value.replace(/'/g, "''")}'` + + sqlColumns.push(sqlColumn) + sqlValues.push(processedValue) + } + } + + return `INSERT INTO ${table} (${sqlColumns.join(', ')}) VALUES (${sqlValues.join(', ')});` +} + +function generateUpdateStatements(row, table) { + const updates = [] + + const processUpdates = (obj, path = []) => { + Object.entries(obj).forEach(([key, value]) => { + const currentPath = [...path, key.replace(/:sql$/, '')] + if (key.endsWith(':sql')) { + updates.push( + `UPDATE ${table} SET value = JSONB_SET(value, '{${currentPath.join( + ',' + )}}', TO_JSONB(${value}), true) WHERE id = ${row.id};` + ) + } else if (typeof value === 'object' && value !== null) { + processUpdates(value, currentPath) + } + }) + } + + Object.entries(row).forEach(([key, value]) => { + if (key.endsWith(':jsonb')) { + processUpdates(value) + } + }) + + return updates +} + +console.info(` +###### ###### ###### ###### ###### ##### ###### ##### + ## ## ## ## ## ## ## ## ## ## ## + ## ## ### ##### ## ## ## ## #### ## ## #### + ## ## ## ## ## ## ## ## ## ## ## + ## ###### ###### ## ## ### ## ## ## ## ## +`) + +const jsonFiles = (await fs.readdir(DIRECTORY_PATH)).filter(file => file.endsWith('.jsonc')) +for (const file of jsonFiles) { + const jsonFilePath = join(DIRECTORY_PATH, file) + const outputSqlFilePath = jsonFilePath.replace('.jsonc', '.sql') + const jsonSource = await fs.readFile(jsonFilePath, 'utf8') + const jsonSourceAsObject = JSON.parse(stripJsonComments(jsonSource)) + + const dataTables = Array.isArray(jsonSourceAsObject) ? jsonSourceAsObject : [jsonSourceAsObject] + const sqlStatementBlocks = dataTables + .map(dataTable => { + const { data: rows, table } = dataTable + + return rows.map(row => { + const insertStatement = generateInsertStatement(row, table) + const updateStatements = generateUpdateStatements(row, table) + + return [insertStatement, ...updateStatements, ''].join('\n') + }) + }) + .flat() + + const sqlSource = sqlStatementBlocks.join('\n') + await fs.writeFile(outputSqlFilePath, sqlSource, 'utf8') + console.info(`[Test Data Generator] SQL Test Data file generated at ${outputSqlFilePath}`) +} + +console.info() From 70eed6eab9e55949fef97a16571ed6571136015c Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 12 Mar 2024 10:11:02 +0100 Subject: [PATCH 27/82] Handle empty trip_gears lists in enrich_logbook --- .../monitorfish/pno_species_and_gears.sql | 2 +- .../test_flows/test_enrich_logbook.py | 31 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql b/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql index aa9f0f1ae3..3e0d2599ac 100644 --- a/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql +++ b/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql @@ -95,7 +95,7 @@ SELECT s.id AS logbook_reports_pno_id, EXTRACT('YEAR' FROM predicted_arrival_datetime_utc)::INTEGER AS year, s.species, - COALESCE(fg.far_gears, dg.dep_gears) AS trip_gears, + COALESCE(fg.far_gears, dg.dep_gears, '[]'::jsonb) AS trip_gears, s.fao_area, s.weight, s.flag_state diff --git a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py index f139aeda6b..f7e8b271a9 100644 --- a/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py +++ b/datascience/tests/test_pipeline/test_flows/test_enrich_logbook.py @@ -126,7 +126,7 @@ def sample_pno_species_and_gears() -> pd.DataFrame: {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, ], [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], - None, + [], [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], @@ -250,7 +250,7 @@ def expected_computed_pno_types() -> pd.DataFrame: {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, ], [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], - None, + [], [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], @@ -359,7 +359,7 @@ def pnos_to_load() -> pd.DataFrame: {"gear": "OTT", "mesh": 120, "dimensions": "250.0"}, {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, ], - None, + [], ], "pno_types": [ [ @@ -440,7 +440,7 @@ def expected_merged_pnos() -> pd.DataFrame: {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, ], [{"gear": "TBB", "mesh": 140, "dimensions": "250.0"}], - None, + [], [{"gear": "OTB", "mesh": 100, "dimensions": "250.0"}], [{"gear": "PTM", "mesh": 70, "dimensions": "250.0"}], [{"gear": "OTM", "mesh": 80, "dimensions": "200.0"}], @@ -573,6 +573,16 @@ def test_compute_pno_types( pd.testing.assert_frame_equal(res, expected_computed_pno_types) +def test_compute_pno_types_with_empty_gears_list_only( + expected_pno_types, sample_pno_species_and_gears, expected_computed_pno_types +): + assert sample_pno_species_and_gears.loc[2, "trip_gears"] == [] + res = compute_pno_types(sample_pno_species_and_gears.loc[[2]], expected_pno_types) + pd.testing.assert_frame_equal( + res, expected_computed_pno_types.loc[[2]].reset_index(drop=True) + ) + + def test_compute_pno_segments( reset_test_data, sample_pno_species_and_gears, @@ -583,6 +593,19 @@ def test_compute_pno_segments( pd.testing.assert_frame_equal(res, expected_computed_pno_segments) +def test_compute_pno_segments_with_empty_gears_only( + reset_test_data, + sample_pno_species_and_gears, + segments, + expected_computed_pno_segments, +): + assert sample_pno_species_and_gears.loc[2, "trip_gears"] == [] + res = compute_pno_segments(sample_pno_species_and_gears.loc[[2]], segments) + pd.testing.assert_frame_equal( + res, expected_computed_pno_segments.loc[[2]].reset_index(drop=True) + ) + + def test_merge_segments_and_types( expected_computed_pno_types, expected_computed_pno_segments, expected_merged_pnos ): From 20d20796dafc0bf8e9d302595c90a623e34d84b9 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 12 Mar 2024 10:23:10 +0100 Subject: [PATCH 28/82] Fix pno_species_and_gears query --- .../pipeline/queries/monitorfish/pno_species_and_gears.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql b/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql index 3e0d2599ac..df5732c96c 100644 --- a/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql +++ b/datascience/src/pipeline/queries/monitorfish/pno_species_and_gears.sql @@ -79,8 +79,8 @@ dep_gears AS ( FROM pno_trips t JOIN logbook_reports dep ON - dep.operation_datetime_utc >= '2024-01-07' - AND dep.operation_datetime_utc < '2024-03-01' + dep.operation_datetime_utc >= :min_trip_date - INTERVAL '1 day' + AND dep.operation_datetime_utc < :max_trip_date AND dep.log_type = 'DEP' AND dep.cfr = t.cfr AND dep.report_id NOT IN (SELECT referenced_report_id FROM deleted_corrected_or_rejected_messages) From 98ed845ab5ba299054913cffe80790d063ac6e84 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Wed, 13 Mar 2024 08:46:36 +0100 Subject: [PATCH 29/82] Add basic prior notification list & filters Backend --- .../domain/entities/logbook/LogbookMessage.kt | 4 + .../entities/logbook/LogbookOperationType.kt | 7 + .../entities/logbook/LogbookTripGear.kt | 7 + .../entities/logbook/LogbookTripSegment.kt | 9 + .../domain/entities/logbook/messages/PNO.kt | 2 + .../domain/entities/vessel/VesselId.kt | 6 + .../domain/filters/LogbookReportFilter.kt | 15 + .../domain/filters/PriorNotificationFilter.kt | 21 + .../domain/filters/ReportingFilter.kt | 10 + .../repositories/LogbookReportRepository.kt | 21 +- .../repositories/ReportingRepository.kt | 27 +- .../domain/repositories/VesselRepository.kt | 9 +- .../GetPriorNotifications.kt | 78 +++ .../dtos/PriorNotification.kt | 22 + .../reporting/GetAllCurrentReportings.kt | 73 +- .../api/bff/PriorNotificationController.kt | 24 + .../LogbookMessageTripGearDataOutput.kt | 18 + .../LogbookMessageTripSegmentDataOutput.kt | 16 + .../outputs/PriorNotificationDataOutput.kt | 50 ++ .../database/entities/LogbookReportEntity.kt | 98 ++- .../JpaLogbookReportRepository.kt | 236 +++++-- .../repositories/JpaReportingRepository.kt | 92 ++- .../repositories/JpaVesselRepository.kt | 12 +- .../interfaces/DBLogbookReportRepository.kt | 21 +- .../interfaces/DBReportingRepository.kt | 14 +- ...46__Create_jsonb_contains_any_function.sql | 48 ++ .../V0.247__Create_unaccent_extension.sql | 1 + ...sert_logbook_raw_messages_and reports.sql} | 30 +- ...5.1__Insert_more_pno_logbook_reports.jsonc | 134 ++++ ...6.5.1__Insert_more_pno_logbook_reports.sql | 13 + .../use_cases/GetLogbookMessagesUTests.kt | 147 ++-- .../monitorfish/domain/use_cases/TestUtils.kt | 640 ++++++++++++------ frontend/src/constants/index.ts | 11 +- .../src/features/Logbook/Logbook.types.ts | 1 + .../features/Logbook/LogbookMessage.types.ts | 100 +++ .../PriorNotification.types.ts | 32 +- .../src/features/PriorNotification/api.ts | 16 +- .../PriorNotificationList/FilterBar.tsx | 23 +- .../VesselRiskFactor.tsx | 140 ++++ .../PriorNotificationList/constants.tsx | 273 ++------ .../PriorNotificationList/index.tsx | 139 ++-- .../components/PriorNotificationList/types.ts | 9 +- .../components/PriorNotificationList/utils.ts | 74 ++ .../src/features/PriorNotification/slice.ts | 14 +- frontend/src/features/SideWindow/index.tsx | 3 - frontend/src/features/Vessel/Vessel.types.ts | 8 + .../features/VesselList/VesselListFilters.tsx | 4 +- .../src/utils/getUrlOrPathWithQueryParams.ts | 39 ++ 48 files changed, 2043 insertions(+), 748 deletions(-) create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripGearDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt create mode 100644 backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql create mode 100644 backend/src/main/resources/db/migration/internal/V0.247__Create_unaccent_extension.sql rename backend/src/main/resources/db/testdata/{V666.5__Insert_logbook.sql => V666.5.0__Insert_logbook_raw_messages_and reports.sql} (99%) create mode 100644 backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc create mode 100644 backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql create mode 100644 frontend/src/features/Logbook/LogbookMessage.types.ts create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/VesselRiskFactor.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/utils.ts create mode 100644 frontend/src/features/Vessel/Vessel.types.ts create mode 100644 frontend/src/utils/getUrlOrPathWithQueryParams.ts diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt index eaf0ea3df2..17d234f0cf 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt @@ -11,7 +11,9 @@ data class LogbookMessage( val tripNumber: String? = null, val referencedReportId: String? = null, var isCorrected: Boolean? = false, + val isEnriched: Boolean, val operationType: LogbookOperationType, + // TODO What's the difference between `operationDateTime`, `integrationDateTime` and `reportDateTime`? Is it in UTC? val operationDateTime: ZonedDateTime, val internalReferenceNumber: String? = null, val externalReferenceNumber: String? = null, @@ -30,4 +32,6 @@ data class LogbookMessage( val transmissionFormat: LogbookTransmissionFormat, val software: String? = null, var isSentByFailoverSoftware: Boolean = false, + val tripGears: List? = listOf(), + val tripSegments: List? = listOf(), ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookOperationType.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookOperationType.kt index 13a722ee4e..ba1e64fa06 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookOperationType.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookOperationType.kt @@ -1,8 +1,15 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook enum class LogbookOperationType { + /** Transmission. */ DAT, + + /** Correction. */ COR, + + /** Suppression. */ DEL, + + /** Acquittement. */ RET, } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt new file mode 100644 index 0000000000..272e81f3fa --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt @@ -0,0 +1,7 @@ +package fr.gouv.cnsp.monitorfish.domain.entities.logbook + +data class LogbookTripGear( + val gear: String, + val mesh: Int, + val dimensions: String, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt new file mode 100644 index 0000000000..f8afaa1b72 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt @@ -0,0 +1,9 @@ +package fr.gouv.cnsp.monitorfish.domain.entities.logbook + +import com.fasterxml.jackson.annotation.JsonProperty + +data class LogbookTripSegment( + val segment: String, + @JsonProperty("segment_name") + val segmentName: String, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt index 3cf3113d75..8e21c84d57 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt @@ -19,6 +19,8 @@ class PNO() : LogbookMessageValue { @JsonProperty("predictedArrivalDatetimeUtc") var predictedArrivalDateTime: ZonedDateTime? = null + @JsonProperty("predictedLandingDatetimeUtc") + var predictedLandingDatetime: ZonedDateTime? = null var tripStartDate: ZonedDateTime? = null } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt new file mode 100644 index 0000000000..2738769cb4 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt @@ -0,0 +1,6 @@ +package fr.gouv.cnsp.monitorfish.domain.entities.vessel + +data class VesselId( + val identifier: VesselIdentifier, + val value: String, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt new file mode 100644 index 0000000000..980f11e1f0 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt @@ -0,0 +1,15 @@ +package fr.gouv.cnsp.monitorfish.domain.filters + +import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselId + +data class LogbookReportFilter( + val flagStates: List? = null, + val integratedAfter: String? = null, + val integratedBefore: String? = null, + val portLocodes: List? = null, + val searchQuery: String? = null, + val specyCodes: List? = null, + val tripSegmentSegments: List? = null, + val tripGearCodes: List? = null, + val vesselId: VesselId? = null, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt new file mode 100644 index 0000000000..3d8472e6d7 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt @@ -0,0 +1,21 @@ +package fr.gouv.cnsp.monitorfish.domain.filters + +data class PriorNotificationFilter( + val countryCodes: List? = null, + val fleetSegmentSegments: List? = null, + val gearCodes: List? = listOf(), + val hasOneOrMoreReportings: Boolean? = null, + val isLessThanTwelveMetersVessel: Boolean? = null, + val isSent: Boolean? = null, + val isVesselPretargeted: Boolean? = null, + val lastControlStartDate: String? = null, + val lastControlEndDate: String? = null, + val portLocodes: List? = null, + val query: String? = null, + val receivedAtStartDate: String? = null, + val receivedAtEndDate: String? = null, + // val seaFrontGroup: SeaFrontGroup | 'EXTRA', + val specyCodes: List? = null, + val searchQuery: String? = null, + // val types: PriorNotification.PriorNotificationType[] +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt new file mode 100644 index 0000000000..c2bff8faa1 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt @@ -0,0 +1,10 @@ +package fr.gouv.cnsp.monitorfish.domain.filters + +import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType + +data class ReportingFilter( + val isArchived: Boolean? = null, + val isDeleted: Boolean? = null, + val types: List? = null, + val vesselId: Int? = null, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt index fe0c1b9716..6a58b3f1ff 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt @@ -3,9 +3,13 @@ package fr.gouv.cnsp.monitorfish.domain.repositories import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.VoyageDatesAndTripNumber import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound +import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification import java.time.ZonedDateTime interface LogbookReportRepository { + fun findAllPriorNotifications(filter: LogbookReportFilter? = null): List + @Throws(NoLogbookFishingTripFound::class) fun findLastTripBeforeDateTime( internalReferenceNumber: String, @@ -25,7 +29,11 @@ interface LogbookReportRepository { ): VoyageDatesAndTripNumber @Throws(NoLogbookFishingTripFound::class) - fun findTripAfterTripNumber(internalReferenceNumber: String, tripNumber: String): VoyageDatesAndTripNumber + fun findTripAfterTripNumber( + internalReferenceNumber: String, + tripNumber: String, + ): VoyageDatesAndTripNumber + fun findAllMessagesByTripNumberBetweenDates( internalReferenceNumber: String, afterDate: ZonedDateTime, @@ -34,10 +42,18 @@ interface LogbookReportRepository { ): List fun findLANAndPNOMessagesNotAnalyzedBy(ruleType: String): List> - fun updateLogbookMessagesAsProcessedByRule(ids: List, ruleType: String) + + fun updateLogbookMessagesAsProcessedByRule( + ids: List, + ruleType: String, + ) + fun findById(id: Long): LogbookMessage + fun findLastMessageDate(): ZonedDateTime + fun findLastTwoYearsTripNumbers(internalReferenceNumber: String): List + fun findFirstAndLastOperationsDatesOfTrip( internalReferenceNumber: String, tripNumber: String, @@ -45,5 +61,6 @@ interface LogbookReportRepository { // For test purpose fun deleteAll() + fun save(message: LogbookMessage) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt index 4917a14531..61c8cb0806 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt @@ -5,21 +5,37 @@ import fr.gouv.cnsp.monitorfish.domain.entities.reporting.InfractionSuspicion import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Observation import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Reporting import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselIdentifier +import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter import java.time.ZonedDateTime interface ReportingRepository { - fun save(alert: PendingAlert, validationDate: ZonedDateTime?) + fun save( + alert: PendingAlert, + validationDate: ZonedDateTime?, + ) + fun save(reporting: Reporting): Reporting - fun update(reportingId: Int, updatedInfractionSuspicion: InfractionSuspicion): Reporting - fun update(reportingId: Int, updatedObservation: Observation): Reporting - fun findAll(): List + + fun update( + reportingId: Int, + updatedInfractionSuspicion: InfractionSuspicion, + ): Reporting + + fun update( + reportingId: Int, + updatedObservation: Observation, + ): Reporting + + fun findAll(filter: ReportingFilter? = null): List + fun findById(reportingId: Int): Reporting - fun findAllCurrent(): List + fun findCurrentAndArchivedByVesselIdentifierEquals( vesselIdentifier: VesselIdentifier, value: String, fromDate: ZonedDateTime, ): List + fun findCurrentAndArchivedByVesselIdEquals( vesselId: Int, fromDate: ZonedDateTime, @@ -33,5 +49,6 @@ interface ReportingRepository { ): List fun archive(id: Int) + fun delete(id: Int) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt index 3a6ec4dd2e..216c88f269 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt @@ -3,8 +3,15 @@ package fr.gouv.cnsp.monitorfish.domain.repositories import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel interface VesselRepository { - fun findVessel(internalReferenceNumber: String, externalReferenceNumber: String, ircs: String): Vessel? + fun findVessel( + internalReferenceNumber: String? = null, + externalReferenceNumber: String? = null, + ircs: String? = null, + ): Vessel? + fun findVesselsByIds(ids: List): List + fun findVessel(vesselId: Int): Vessel? + fun search(searched: String): List } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt new file mode 100644 index 0000000000..759e06cf9e --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt @@ -0,0 +1,78 @@ +package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification + +import fr.gouv.cnsp.monitorfish.config.UseCase +import fr.gouv.cnsp.monitorfish.domain.exceptions.CodeNotFoundException +import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter +import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification +import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.* +import org.locationtech.jts.geom.Coordinate +import org.locationtech.jts.geom.GeometryFactory + +@UseCase +class GetPriorNotifications( + private val facadeAreasRepository: JpaFacadeAreasRepository, + private val logbookReportRepository: JpaLogbookReportRepository, + private val portRepository: JpaPortRepository, + private val reportingRepository: JpaReportingRepository, + private val riskFactorRepository: JpaRiskFactorsRepository, + private val vesselRepository: JpaVesselRepository, +) { + fun execute(filter: LogbookReportFilter): List { + val priorNotifications = + logbookReportRepository.findAllPriorNotifications(filter).map { priorNotification -> + val port = + try { + priorNotification.portLocode?.let { + portRepository.find(it) + } + } catch (e: CodeNotFoundException) { + null + } + + // TODO Doesn't seem to work. + val seaFront = + port?.latitude?.let { latitude -> + port.longitude?.let { longitude -> + val point = GeometryFactory().createPoint(Coordinate(longitude, latitude)) + + facadeAreasRepository.findByIncluding(point).firstOrNull()?.facade + } + } + + val vessel = + vesselRepository.findVessel( + priorNotification.logbookMessage?.internalReferenceNumber, + priorNotification.logbookMessage?.externalReferenceNumber, + priorNotification.logbookMessage?.ircs, + ) + + val reportingsCount = + vessel?.id.let { vesselId -> + val reportingsFilter = + ReportingFilter( + isArchived = false, + isDeleted = false, + vesselId = vesselId, + ) + + reportingRepository.findAll(reportingsFilter).count() + } + + val vesselRiskFactor = + priorNotification.logbookMessage?.internalReferenceNumber?.let { + riskFactorRepository.findVesselRiskFactors(it) + } + + priorNotification.copy( + port = port, + reportingsCount = reportingsCount, + seaFront = seaFront, + vessel = vessel, + vesselRiskFactor = vesselRiskFactor, + ) + } + + return priorNotifications + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt new file mode 100644 index 0000000000..7f9f6250f5 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt @@ -0,0 +1,22 @@ +package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripSegment +import fr.gouv.cnsp.monitorfish.domain.entities.port.Port +import fr.gouv.cnsp.monitorfish.domain.entities.risk_factor.VesselRiskFactor +import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel + +data class PriorNotification( + val id: Long, + val logbookMessage: LogbookMessage?, + // TODO It's only used for later use case resolution and not exposed in the data outpur. Maybe find a way to remove it from the DTO? + val portLocode: String?, + val reportingsCount: Int? = null, + val tripGears: List, + val tripSegments: List, + val port: Port? = null, + val seaFront: String? = null, + val vessel: Vessel? = null, + val vesselRiskFactor: VesselRiskFactor? = null, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt index 08de9658ee..5aa4ccca9e 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt @@ -6,6 +6,7 @@ import fr.gouv.cnsp.monitorfish.domain.entities.reporting.InfractionSuspicionOrO import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Reporting import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselIdentifier +import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter import fr.gouv.cnsp.monitorfish.domain.repositories.LastPositionRepository import fr.gouv.cnsp.monitorfish.domain.repositories.ReportingRepository import fr.gouv.cnsp.monitorfish.domain.use_cases.control_units.GetAllControlUnits @@ -21,46 +22,54 @@ class GetAllCurrentReportings( private val logger: Logger = LoggerFactory.getLogger(GetAllCurrentReportings::class.java) fun execute(): List> { - val currents = reportingRepository.findAllCurrent() + val filter = + ReportingFilter( + isArchived = false, + isDeleted = false, + types = listOf(ReportingType.INFRACTION_SUSPICION, ReportingType.ALERT), + ) + + val currents = reportingRepository.findAll(filter) val controlUnits = getAllControlUnits.execute() currents.forEach { - it.underCharter = try { - when (it.vesselIdentifier) { - VesselIdentifier.INTERNAL_REFERENCE_NUMBER -> { - require(it.internalReferenceNumber != null) { - "The fields 'internalReferenceNumber' must be not null when the vessel identifier is INTERNAL_REFERENCE_NUMBER." + it.underCharter = + try { + when (it.vesselIdentifier) { + VesselIdentifier.INTERNAL_REFERENCE_NUMBER -> { + require(it.internalReferenceNumber != null) { + "The fields 'internalReferenceNumber' must be not null when the vessel identifier is INTERNAL_REFERENCE_NUMBER." + } + lastPositionRepository.findUnderCharterForVessel( + it.vesselIdentifier, + it.internalReferenceNumber, + ) } - lastPositionRepository.findUnderCharterForVessel( - it.vesselIdentifier, - it.internalReferenceNumber, - ) - } - VesselIdentifier.IRCS -> { - require(it.ircs != null) { - "The fields 'ircs' must be not null when the vessel identifier is IRCS." + VesselIdentifier.IRCS -> { + require(it.ircs != null) { + "The fields 'ircs' must be not null when the vessel identifier is IRCS." + } + lastPositionRepository.findUnderCharterForVessel(it.vesselIdentifier, it.ircs) } - lastPositionRepository.findUnderCharterForVessel(it.vesselIdentifier, it.ircs) - } - VesselIdentifier.EXTERNAL_REFERENCE_NUMBER -> { - require(it.externalReferenceNumber != null) { - "The fields 'externalReferenceNumber' must be not null when the vessel identifier is EXTERNAL_REFERENCE_NUMBER." + VesselIdentifier.EXTERNAL_REFERENCE_NUMBER -> { + require(it.externalReferenceNumber != null) { + "The fields 'externalReferenceNumber' must be not null when the vessel identifier is EXTERNAL_REFERENCE_NUMBER." + } + lastPositionRepository.findUnderCharterForVessel( + it.vesselIdentifier, + it.externalReferenceNumber, + ) } - lastPositionRepository.findUnderCharterForVessel( - it.vesselIdentifier, - it.externalReferenceNumber, - ) + else -> null } - else -> null - } - } catch (e: Throwable) { - logger.error( - "Last position not found for vessel \"${it.internalReferenceNumber}/${it.ircs}/${it.externalReferenceNumber}\" " + - "and vessel identifier \"${it.vesselIdentifier}\": ${e.message}", - ) + } catch (e: Throwable) { + logger.error( + "Last position not found for vessel \"${it.internalReferenceNumber}/${it.ircs}/${it.externalReferenceNumber}\" " + + "and vessel identifier \"${it.vesselIdentifier}\": ${e.message}", + ) - null - } + null + } } return currents.map { reporting -> diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt new file mode 100644 index 0000000000..6606e97b5e --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt @@ -0,0 +1,24 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.bff + +import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotifications +import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.PriorNotificationDataOutput +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/bff/v1/prior-notifications") +@Tag(name = "Prior notifications endpoints") +class PriorNotificationController(private val getPriorNotifications: GetPriorNotifications) { + @GetMapping("") + @Operation(summary = "Get all prior notifications") + fun getAll( + @ModelAttribute filter: LogbookReportFilter, + ): List { + return getPriorNotifications.execute(filter).map { PriorNotificationDataOutput.fromPriorNotification(it) } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripGearDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripGearDataOutput.kt new file mode 100644 index 0000000000..6f5cfad346 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripGearDataOutput.kt @@ -0,0 +1,18 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear + +class LogbookMessageTripGearDataOutput( + val gear: String, + val mesh: Int, + val dimensions: String, +) { + companion object { + fun fromLogbookTripGear(logbookTripGear: LogbookTripGear) = + LogbookMessageTripGearDataOutput( + gear = logbookTripGear.gear, + mesh = logbookTripGear.mesh, + dimensions = logbookTripGear.dimensions, + ) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt new file mode 100644 index 0000000000..7313d502c5 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt @@ -0,0 +1,16 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripSegment + +class LogbookMessageTripSegmentDataOutput( + val segment: String, + val segmentName: String, +) { + companion object { + fun fromLogbookTripSegment(logbookTripSegment: LogbookTripSegment) = + LogbookMessageTripSegmentDataOutput( + segment = logbookTripSegment.segment, + segmentName = logbookTripSegment.segmentName, + ) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt new file mode 100644 index 0000000000..caa8fa6522 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt @@ -0,0 +1,50 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification + +class PriorNotificationDataOutput( + val id: Long, + val logbookMessage: LogbookMessageDataOutput?, + val port: PortDataOutput?, + val reportingsCount: Int, + val seaFront: String?, + val tripGears: List, + val tripSegments: List, + val vessel: VesselDataOutput?, + val vesselRiskFactor: RiskFactorDataOutput?, +) { + companion object { + fun fromPriorNotification(priorNotification: PriorNotification): PriorNotificationDataOutput { + val logbookMessage = + priorNotification.logbookMessage?.let { + LogbookMessageDataOutput.fromLogbookMessage( + it, + ) + } + val port = priorNotification.port?.let { PortDataOutput.fromPort(it) } + val tripGears = + priorNotification.tripGears.map { LogbookMessageTripGearDataOutput.fromLogbookTripGear(it) } + val tripSegments = + priorNotification.tripSegments.map { LogbookMessageTripSegmentDataOutput.fromLogbookTripSegment(it) } + val vessel = priorNotification.vessel?.let { VesselDataOutput.fromVessel(it) } + val vesselRiskFactor = + priorNotification.vesselRiskFactor?.let { + RiskFactorDataOutput.fromVesselRiskFactor( + it, + ) + } + + return PriorNotificationDataOutput( + id = priorNotification.id, + logbookMessage, + port, + reportingsCount = requireNotNull(priorNotification.reportingsCount), + seaFront = priorNotification.seaFront, + tripGears, + tripSegments, + vessel, + vesselRiskFactor, + ) + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt index 442131a6bc..cd89084fdd 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt @@ -1,10 +1,9 @@ package fr.gouv.cnsp.monitorfish.infrastructure.database.entities import com.fasterxml.jackson.databind.ObjectMapper -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTransmissionFormat +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.* import fr.gouv.cnsp.monitorfish.domain.mappers.ERSMapper.getERSMessageValueFromJSON +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification import io.hypersistence.utils.hibernate.type.array.ListArrayType import io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType import io.hypersistence.utils.hibernate.type.json.JsonBinaryType @@ -21,7 +20,6 @@ data class LogbookReportEntity( @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "logbook_report_id_seq") @Column(name = "id") val id: Long? = null, - @Column(name = "operation_number") val operationNumber: String, @Column(name = "trip_number") @@ -67,10 +65,20 @@ data class LogbookReportEntity( val transmissionFormat: LogbookTransmissionFormat, @Column(name = "software") val software: String? = null, + @Column(name = "enriched") + val isEnriched: Boolean, + @Type(JsonBinaryType::class) + @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") + val tripGears: String? = null, + @Type(JsonBinaryType::class) + @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") + val tripSegments: String? = null, ) { - companion object { - fun fromLogbookMessage(mapper: ObjectMapper, logbookMessage: LogbookMessage) = LogbookReportEntity( + fun fromLogbookMessage( + mapper: ObjectMapper, + logbookMessage: LogbookMessage, + ) = LogbookReportEntity( internalReferenceNumber = logbookMessage.internalReferenceNumber, referencedReportId = logbookMessage.referencedReportId, externalReferenceNumber = logbookMessage.externalReferenceNumber, @@ -90,29 +98,63 @@ data class LogbookReportEntity( message = mapper.writeValueAsString(logbookMessage.message), software = logbookMessage.software, transmissionFormat = logbookMessage.transmissionFormat, + isEnriched = logbookMessage.isEnriched, + ) + } + + fun toLogbookMessage(mapper: ObjectMapper) = + LogbookMessage( + id = id!!, + internalReferenceNumber = internalReferenceNumber, + referencedReportId = referencedReportId, + externalReferenceNumber = externalReferenceNumber, + ircs = ircs, + operationDateTime = operationDateTime.atZone(UTC), + reportDateTime = reportDateTime?.atZone(UTC), + integrationDateTime = integrationDateTime.atZone(UTC), + vesselName = vesselName, + operationType = operationType, + reportId = reportId, + operationNumber = operationNumber, + tripNumber = tripNumber, + flagState = flagState, + imo = imo, + messageType = messageType, + analyzedByRules = analyzedByRules ?: listOf(), + message = getERSMessageValueFromJSON(mapper, message, messageType, operationType), + software = software, + transmissionFormat = transmissionFormat, + isEnriched = isEnriched, + tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java), + tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java), + ) + + fun toPriorNotification(mapper: ObjectMapper): PriorNotification { + val messageAsJsonNode = mapper.readTree(message) + val portLocode = messageAsJsonNode.get("port")?.asText() + val logbookMessage = toLogbookMessage(mapper) + val tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java) + val tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java) + + return PriorNotification( + id = id!!, + logbookMessage = logbookMessage, + portLocode = portLocode, + tripGears = tripGears, + tripSegments = tripSegments, ) } - fun toLogbookMessage(mapper: ObjectMapper) = LogbookMessage( - id = id!!, - internalReferenceNumber = internalReferenceNumber, - referencedReportId = referencedReportId, - externalReferenceNumber = externalReferenceNumber, - ircs = ircs, - operationDateTime = operationDateTime.atZone(UTC), - reportDateTime = reportDateTime?.atZone(UTC), - integrationDateTime = integrationDateTime.atZone(UTC), - vesselName = vesselName, - operationType = operationType, - reportId = reportId, - operationNumber = operationNumber, - tripNumber = tripNumber, - flagState = flagState, - imo = imo, - messageType = messageType, - analyzedByRules = analyzedByRules ?: listOf(), - message = getERSMessageValueFromJSON(mapper, message, messageType, operationType), - software = software, - transmissionFormat = transmissionFormat, - ) + private fun deserializeJSONList( + mapper: ObjectMapper, + json: String?, + clazz: Class, + ): List = + json?.let { + mapper.readValue( + json, + mapper.typeFactory + .constructCollectionType(MutableList::class.java, clazz), + ) + } ?: listOf() } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt index b9621418bc..cd235fe315 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt @@ -7,10 +7,14 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType import fr.gouv.cnsp.monitorfish.domain.entities.logbook.VoyageDatesAndTripNumber import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound +import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter import fr.gouv.cnsp.monitorfish.domain.repositories.LogbookReportRepository +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.LogbookReportEntity import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBLogbookReportRepository +import jakarta.persistence.EntityManager import jakarta.transaction.Transactional +import org.springframework.beans.factory.annotation.Autowired import org.springframework.cache.annotation.Cacheable import org.springframework.dao.EmptyResultDataAccessException import org.springframework.data.domain.PageRequest @@ -22,19 +26,149 @@ import java.time.ZonedDateTime @Repository class JpaLogbookReportRepository( private val dbERSRepository: DBLogbookReportRepository, + @Autowired private val entityManager: EntityManager, private val mapper: ObjectMapper, ) : LogbookReportRepository { - private val postgresChunkSize = 5000 - override fun findLastTripBeforeDateTime(internalReferenceNumber: String, beforeDateTime: ZonedDateTime): VoyageDatesAndTripNumber { + override fun findAllPriorNotifications(filter: LogbookReportFilter?): List { + val criteriaBuilder = entityManager.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(LogbookReportEntity::class.java) + val logbookReportEntity = criteriaQuery.from(LogbookReportEntity::class.java) + + val predicates = mutableListOf(criteriaBuilder.isTrue(criteriaBuilder.literal(true))) + // Only enriched PNO messages + predicates.add( + criteriaBuilder.and( + criteriaBuilder.equal(logbookReportEntity.get("messageType"), "PNO"), + criteriaBuilder.equal(logbookReportEntity.get("isEnriched"), true), + ), + ) + + filter?.let { + it.flagStates?.let { flagStates -> + predicates.add(logbookReportEntity.get("flagState").`in`(flagStates)) + } + + it.integratedAfter?.let { integratedAfter -> + predicates.add( + criteriaBuilder.greaterThanOrEqualTo( + logbookReportEntity.get("integrationDateTime"), + ZonedDateTime.parse(integratedAfter).withZoneSameInstant(UTC), + ), + ) + } + + it.integratedBefore?.let { integratedBefore -> + predicates.add( + criteriaBuilder.lessThanOrEqualTo( + logbookReportEntity.get("integrationDateTime"), + ZonedDateTime.parse(integratedBefore).withZoneSameInstant(UTC), + ), + ) + } + + it.portLocodes?.let { portLocodes -> + predicates.add( + criteriaBuilder.function( + "jsonb_extract_path_text", + String::class.java, + logbookReportEntity.get("message"), + criteriaBuilder.literal("port"), + ).`in`(portLocodes), + ) + } + + it.searchQuery?.let { searchQuery -> + val normalizedPath = + criteriaBuilder.lower( + criteriaBuilder.function( + "unaccent", + String::class.java, + logbookReportEntity.get("vesselName"), + ), + ) + val searchQueryPattern = "%${searchQuery.trim()}%" + val normalizedSearchQuery = + criteriaBuilder.lower( + criteriaBuilder.function( + "unaccent", + String::class.java, + criteriaBuilder.literal(searchQueryPattern), + ), + ) + + predicates.add( + criteriaBuilder.like( + normalizedPath, + normalizedSearchQuery, + ), + ) + } + + it.specyCodes?.let { specyCodes -> + predicates.add( + criteriaBuilder.isTrue( + criteriaBuilder.function( + "jsonb_contains_any", + Boolean::class.java, + logbookReportEntity.get("message"), + criteriaBuilder.literal(arrayOf("catchOnboard")), + criteriaBuilder.literal("species"), + criteriaBuilder.literal(specyCodes.toTypedArray()), + ), + ), + ) + } + + it.tripGearCodes?.let { tripGearCodes -> + predicates.add( + criteriaBuilder.isTrue( + criteriaBuilder.function( + "jsonb_contains_any", + Boolean::class.java, + logbookReportEntity.get("tripGears"), + criteriaBuilder.literal(emptyArray()), + criteriaBuilder.literal("gear"), + criteriaBuilder.literal(tripGearCodes.toTypedArray()), + ), + ), + ) + } + + it.tripSegmentSegments?.let { tripSegmentSegments -> + predicates.add( + criteriaBuilder.isTrue( + criteriaBuilder.function( + "jsonb_contains_any", + Boolean::class.java, + logbookReportEntity.get("tripSegments"), + criteriaBuilder.literal(emptyArray()), + criteriaBuilder.literal("segment"), + criteriaBuilder.literal(tripSegmentSegments.toTypedArray()), + ), + ), + ) + } + } + + criteriaQuery.select(logbookReportEntity).where(*predicates.toTypedArray()) + + return entityManager.createQuery(criteriaQuery).resultList.map { it.toPriorNotification(mapper) } + } + + override fun findLastTripBeforeDateTime( + internalReferenceNumber: String, + beforeDateTime: ZonedDateTime, + ): VoyageDatesAndTripNumber { try { if (internalReferenceNumber.isNotEmpty()) { - val lastTrip = dbERSRepository.findTripsBeforeDatetime( - internalReferenceNumber, - beforeDateTime.toInstant(), - PageRequest.of(0, 1), - ).first() + val lastTrip = + dbERSRepository.findTripsBeforeDatetime( + internalReferenceNumber, + beforeDateTime.toInstant(), + PageRequest.of(0, 1), + ).first() return VoyageDatesAndTripNumber( lastTrip.tripNumber, @@ -58,15 +192,17 @@ class JpaLogbookReportRepository( ): VoyageDatesAndTripNumber { try { if (internalReferenceNumber.isNotEmpty()) { - val previousTripNumber = dbERSRepository.findPreviousTripNumber( - internalReferenceNumber, - tripNumber, - PageRequest.of(0, 1), - ).first().tripNumber - val previousTrip = dbERSRepository.findFirstAndLastOperationsDatesOfTrip( - internalReferenceNumber, - previousTripNumber, - ) + val previousTripNumber = + dbERSRepository.findPreviousTripNumber( + internalReferenceNumber, + tripNumber, + PageRequest.of(0, 1), + ).first().tripNumber + val previousTrip = + dbERSRepository.findFirstAndLastOperationsDatesOfTrip( + internalReferenceNumber, + previousTripNumber, + ) return VoyageDatesAndTripNumber( previousTripNumber, @@ -92,10 +228,11 @@ class JpaLogbookReportRepository( ): VoyageDatesAndTripNumber { try { if (internalReferenceNumber.isNotEmpty()) { - val nextTrip = dbERSRepository.findFirstAndLastOperationsDatesOfTrip( - internalReferenceNumber, - tripNumber, - ) + val nextTrip = + dbERSRepository.findFirstAndLastOperationsDatesOfTrip( + internalReferenceNumber, + tripNumber, + ) return VoyageDatesAndTripNumber( tripNumber, @@ -121,15 +258,17 @@ class JpaLogbookReportRepository( ): VoyageDatesAndTripNumber { try { if (internalReferenceNumber.isNotEmpty()) { - val nextTripNumber = dbERSRepository.findNextTripNumber( - internalReferenceNumber, - tripNumber, - PageRequest.of(0, 1), - ).first().tripNumber - val nextTrip = dbERSRepository.findFirstAndLastOperationsDatesOfTrip( - internalReferenceNumber, - nextTripNumber, - ) + val nextTripNumber = + dbERSRepository.findNextTripNumber( + internalReferenceNumber, + tripNumber, + PageRequest.of(0, 1), + ).first().tripNumber + val nextTrip = + dbERSRepository.findFirstAndLastOperationsDatesOfTrip( + internalReferenceNumber, + nextTripNumber, + ) return VoyageDatesAndTripNumber( nextTripNumber, @@ -181,26 +320,31 @@ class JpaLogbookReportRepository( override fun findLANAndPNOMessagesNotAnalyzedBy(ruleType: String): List> { val lanAndPnoMessages = dbERSRepository.findAllLANAndPNONotProcessedByRule(ruleType) - val lanAndPnoMessagesWithoutCorrectedMessages = lanAndPnoMessages.filter { lanMessage -> - getCorrectedMessageIfAvailable(lanMessage, lanAndPnoMessages) - } + val lanAndPnoMessagesWithoutCorrectedMessages = + lanAndPnoMessages.filter { lanMessage -> + getCorrectedMessageIfAvailable(lanMessage, lanAndPnoMessages) + } return lanAndPnoMessagesWithoutCorrectedMessages.filter { it.internalReferenceNumber != null && it.tripNumber != null && it.messageType == LogbookMessageTypeMapping.LAN.name }.map { lanMessage -> - val pnoMessage = lanAndPnoMessagesWithoutCorrectedMessages.singleOrNull { message -> - message.internalReferenceNumber == lanMessage.internalReferenceNumber && - message.tripNumber == lanMessage.tripNumber && - message.messageType == LogbookMessageTypeMapping.PNO.name - } + val pnoMessage = + lanAndPnoMessagesWithoutCorrectedMessages.singleOrNull { message -> + message.internalReferenceNumber == lanMessage.internalReferenceNumber && + message.tripNumber == lanMessage.tripNumber && + message.messageType == LogbookMessageTypeMapping.PNO.name + } Pair(lanMessage.toLogbookMessage(mapper), pnoMessage?.toLogbookMessage(mapper)) } } - override fun updateLogbookMessagesAsProcessedByRule(ids: List, ruleType: String) { + override fun updateLogbookMessagesAsProcessedByRule( + ids: List, + ruleType: String, + ) { ids.chunked(postgresChunkSize).forEach { dbERSRepository.updateERSMessagesAsProcessedByRule(it, ruleType) } @@ -221,11 +365,12 @@ class JpaLogbookReportRepository( ): ZonedDateTime { try { if (internalReferenceNumber.isNotEmpty()) { - val lastTrip = dbERSRepository.findTripsBeforeDatetime( - internalReferenceNumber, - beforeDateTime.toInstant(), - PageRequest.of(0, 1), - ).first() + val lastTrip = + dbERSRepository.findTripsBeforeDatetime( + internalReferenceNumber, + beforeDateTime.toInstant(), + PageRequest.of(0, 1), + ).first() return dbERSRepository.findFirstAcknowledgedDateOfTrip( internalReferenceNumber, @@ -262,7 +407,10 @@ class JpaLogbookReportRepository( dbERSRepository.save(LogbookReportEntity.fromLogbookMessage(mapper, message)) } - private fun getCorrectedMessageIfAvailable(pnoMessage: LogbookReportEntity, messages: List): Boolean { + private fun getCorrectedMessageIfAvailable( + pnoMessage: LogbookReportEntity, + messages: List, + ): Boolean { return if (pnoMessage.operationType == LogbookOperationType.DAT) { !messages.any { it.operationType == LogbookOperationType.COR && it.referencedReportId == pnoMessage.reportId diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt index a0ddc9e143..af9e6d8051 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt @@ -7,20 +7,26 @@ import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Observation import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Reporting import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselIdentifier +import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter import fr.gouv.cnsp.monitorfish.domain.repositories.ReportingRepository import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ReportingEntity import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBReportingRepository +import jakarta.persistence.EntityManager import jakarta.transaction.Transactional +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository import java.time.ZonedDateTime @Repository class JpaReportingRepository( private val dbReportingRepository: DBReportingRepository, + @Autowired private val entityManager: EntityManager, private val mapper: ObjectMapper, ) : ReportingRepository { - - override fun save(alert: PendingAlert, validationDate: ZonedDateTime?) { + override fun save( + alert: PendingAlert, + validationDate: ZonedDateTime?, + ) { dbReportingRepository.save(ReportingEntity.fromPendingAlert(alert, validationDate, mapper)) } @@ -29,7 +35,10 @@ class JpaReportingRepository( } @Transactional - override fun update(reportingId: Int, updatedInfractionSuspicion: InfractionSuspicion): Reporting { + override fun update( + reportingId: Int, + updatedInfractionSuspicion: InfractionSuspicion, + ): Reporting { dbReportingRepository.update( reportingId, mapper.writeValueAsString(updatedInfractionSuspicion), @@ -40,7 +49,10 @@ class JpaReportingRepository( } @Transactional - override fun update(reportingId: Int, updatedObservation: Observation): Reporting { + override fun update( + reportingId: Int, + updatedObservation: Observation, + ): Reporting { dbReportingRepository.update( reportingId, mapper.writeValueAsString(updatedObservation), @@ -50,18 +62,73 @@ class JpaReportingRepository( return dbReportingRepository.findById(reportingId).get().toReporting(mapper) } - override fun findAll(): List { - return dbReportingRepository.findAll().map { it.toReporting(mapper) } + override fun findAll(filter: ReportingFilter?): List { + val criteriaBuilder = entityManager.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(ReportingEntity::class.java) + val reportingEntity = criteriaQuery.from(ReportingEntity::class.java) + + val predicates = mutableListOf(criteriaBuilder.isTrue(criteriaBuilder.literal(true))) + filter?.let { + it.isArchived?.let { isArchived -> + predicates.add(criteriaBuilder.equal(reportingEntity.get("isArchived"), isArchived)) + } + + it.isDeleted?.let { isDeleted -> + predicates.add(criteriaBuilder.equal(reportingEntity.get("isDeleted"), isDeleted)) + } + + it.types?.let { types -> + predicates.add(reportingEntity.get("type").`in`(*types.toTypedArray())) + } + + it.vesselId?.let { vesselId -> + predicates.add( + criteriaBuilder.equal( + reportingEntity.get("vesselId"), + vesselId, + ), + ) + } + +// it.vesselId?.let { vesselId -> +// when (vesselId.identifier) { +// VesselIdentifier.INTERNAL_REFERENCE_NUMBER -> { +// predicates.add( +// criteriaBuilder.equal( +// reportingEntity.get("internalReferenceNumber"), +// vesselId.toString(), +// ), +// ) +// } +// VesselIdentifier.IRCS -> { +// predicates.add( +// criteriaBuilder.equal( +// reportingEntity.get("ircs"), +// vesselId.toString(), +// ), +// ) +// } +// VesselIdentifier.EXTERNAL_REFERENCE_NUMBER -> { +// predicates.add( +// criteriaBuilder.equal( +// reportingEntity.get("externalReferenceNumber"), +// vesselId.toString(), +// ), +// ) +// } +// } +// } + } + + criteriaQuery.select(reportingEntity).where(*predicates.toTypedArray()) + + return entityManager.createQuery(criteriaQuery).resultList.map { it.toReporting(mapper) } } override fun findById(reportingId: Int): Reporting { return dbReportingRepository.findById(reportingId).get().toReporting(mapper) } - override fun findAllCurrent(): List { - return dbReportingRepository.findAllCurrentReportings().map { it.toReporting(mapper) } - } - override fun findCurrentAndArchivedByVesselIdentifierEquals( vesselIdentifier: VesselIdentifier, value: String, @@ -73,7 +140,10 @@ class JpaReportingRepository( } } - override fun findCurrentAndArchivedByVesselIdEquals(vesselId: Int, fromDate: ZonedDateTime): List { + override fun findCurrentAndArchivedByVesselIdEquals( + vesselId: Int, + fromDate: ZonedDateTime, + ): List { return dbReportingRepository .findCurrentAndArchivedByVesselId(vesselId, fromDate.toInstant()).map { it.toReporting(mapper) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt index b6b5ad9982..e5f219c143 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt @@ -15,8 +15,12 @@ class JpaVesselRepository(private val dbVesselRepository: DBVesselRepository) : private val logger: Logger = LoggerFactory.getLogger(JpaVesselRepository::class.java) @Cacheable(value = ["vessel"]) - override fun findVessel(internalReferenceNumber: String, externalReferenceNumber: String, ircs: String): Vessel? { - if (internalReferenceNumber.isNotEmpty()) { + override fun findVessel( + internalReferenceNumber: String?, + externalReferenceNumber: String?, + ircs: String?, + ): Vessel? { + if (!internalReferenceNumber.isNullOrEmpty()) { try { return dbVesselRepository.findByInternalReferenceNumber(internalReferenceNumber).toVessel() } catch (e: EmptyResultDataAccessException) { @@ -24,7 +28,7 @@ class JpaVesselRepository(private val dbVesselRepository: DBVesselRepository) : } } - if (ircs.isNotEmpty()) { + if (!ircs.isNullOrEmpty()) { try { return dbVesselRepository.findByIrcs(ircs).toVessel() } catch (e: EmptyResultDataAccessException) { @@ -32,7 +36,7 @@ class JpaVesselRepository(private val dbVesselRepository: DBVesselRepository) : } } - if (externalReferenceNumber.isNotEmpty()) { + if (!externalReferenceNumber.isNullOrEmpty()) { try { return dbVesselRepository.findByExternalReferenceNumberIgnoreCaseContaining( externalReferenceNumber, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt index c94eb8a2a9..b6402a720b 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt @@ -93,7 +93,10 @@ interface DBLogbookReportRepository : dc.transmission_format = 'FLUX'""", nativeQuery = true, ) - fun findFirstAcknowledgedDateOfTrip(internalReferenceNumber: String, tripNumber: String): Instant + fun findFirstAcknowledgedDateOfTrip( + internalReferenceNumber: String, + tripNumber: String, + ): Instant @Query( """WITH dat_cor AS ( @@ -162,7 +165,10 @@ interface DBLogbookReportRepository : "update logbook_reports set analyzed_by_rules = array_append(analyzed_by_rules, :ruleType) where id in (:ids)", nativeQuery = true, ) - fun updateERSMessagesAsProcessedByRule(ids: List, ruleType: String) + fun updateERSMessagesAsProcessedByRule( + ids: List, + ruleType: String, + ) @Query( """SELECT distinct e.trip_number @@ -176,4 +182,15 @@ interface DBLogbookReportRepository : nativeQuery = true, ) fun findLastTwoYearsTripNumbers(internalReferenceNumber: String): List + + @Query( + """ + SELECT * + FROM logbook_reports + WHERE log_type = 'PNO' + ORDER BY operation_datetime_utc DESC + """, + nativeQuery = true, + ) + fun findPNOMessages(): List } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBReportingRepository.kt index 2a6c4fae4f..788d2850fe 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBReportingRepository.kt @@ -64,14 +64,6 @@ interface DBReportingRepository : CrudRepository { fromDate: Instant, ): List - @Query( - value = """ - SELECT * FROM reportings WHERE archived IS FALSE AND deleted IS FALSE AND type IN ('INFRACTION_SUSPICION', 'ALERT') - """, - nativeQuery = true, - ) - fun findAllCurrentReportings(): List - @Modifying(clearAutomatically = true) @Query( value = """ @@ -105,5 +97,9 @@ interface DBReportingRepository : CrudRepository { """, nativeQuery = true, ) - fun update(id: Int, value: String, type: String) + fun update( + id: Int, + value: String, + type: String, + ) } diff --git a/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql b/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql new file mode 100644 index 0000000000..e7a2b10b57 --- /dev/null +++ b/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql @@ -0,0 +1,48 @@ +CREATE OR REPLACE FUNCTION jsonb_contains_any( + jsonb_data jsonb, + collection_path text[], + collection_key text, + search_values text[] +) +RETURNS boolean AS $$ + DECLARE + collection_value jsonb; + key_path text; + search_value text; + BEGIN + -- If the JSONB data is NULL, then the search is impossible + IF jsonb_data IS NULL THEN + RETURN FALSE; + END IF; + + collection_value := jsonb_data; + FOREACH key_path IN ARRAY collection_path + LOOP + collection_value := collection_value -> key_path; + + -- If any of the nested key value is NULL before reaching the collection, then the search is impossible + IF collection_value IS NULL THEN + RETURN FALSE; + END IF; + END LOOP; + + -- If the collection value is not an array, then the search is impossible + IF jsonb_typeof(collection_value) != 'array' THEN + RETURN FALSE; + END IF; + + FOREACH search_value IN ARRAY search_values + LOOP + IF EXISTS ( + SELECT 1 + FROM jsonb_array_elements(collection_value) as element + WHERE element ->> collection_key = search_value + ) THEN + RETURN TRUE; + END IF; + END LOOP; + + RETURN FALSE; + END; +$$ LANGUAGE plpgsql IMMUTABLE; + diff --git a/backend/src/main/resources/db/migration/internal/V0.247__Create_unaccent_extension.sql b/backend/src/main/resources/db/migration/internal/V0.247__Create_unaccent_extension.sql new file mode 100644 index 0000000000..b700d5127e --- /dev/null +++ b/backend/src/main/resources/db/migration/internal/V0.247__Create_unaccent_extension.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS unaccent; diff --git a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql b/backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql similarity index 99% rename from backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql rename to backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql index 576a6b47be..866f31cec8 100644 --- a/backend/src/main/resources/db/testdata/V666.5__Insert_logbook.sql +++ b/backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql @@ -729,7 +729,7 @@ SET enriched = true, trip_gears = '[]'::jsonb, trip_segments = '[]'::jsonb, - pno_types = '[]'::jsonb + value = jsonb_set(value, '{pnoTypes}', '[]'::jsonb) WHERE operation_number IN ('OOF20191011059902', 'OOF20190439686457', 'd5c3b039-aaee-4cca-bcae-637f5fe574f5'); UPDATE logbook_reports @@ -737,16 +737,20 @@ SET enriched = true, trip_gears = '[{"gear": "GTR", "mesh": 100, "dimensions": "250;180"}, {"gear": "GTR", "mesh": 120.5, "dimensions": "250;280"}]'::jsonb, trip_segments = '[{"segment": "NWW01", "segment_name": "Chalutiers de fond"}, {"segment": "PEL01", "segment_name": "Chalutiers pélagiques"}]'::jsonb, - pno_types = '[ - { - "pno_type_name": "Préavis type X", - "minimum_notification_period": 4.0, - "has_designated_ports": false - }, - { - "pno_type_name": "Préavis type Y", - "minimum_notification_period": 8.0, - "has_designated_ports": true - } - ]'::jsonb + value = jsonb_set( + value, + '{pnoTypes}', + '[ + { + "pnoTypeName": "Préavis type X", + "minimumNotificationPeriod": 4.0, + "hasDesignated_ports": false + }, + { + "pnoTypeName": "Préavis type Y", + "minimumNotificationPeriod": 8.0, + "hasDesignated_ports": true + } + ]'::jsonb + ) WHERE operation_number = 'OOF20191011059902'; diff --git a/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc new file mode 100644 index 0000000000..3df5d13c0e --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc @@ -0,0 +1,134 @@ +[ + { + "table": "logbook_raw_messages", + "data": [{ "operation_number": "FAKE_OPERATION_101" }, { "operation_number": "FAKE_OPERATION_102" }] + }, + { + "table": "logbook_reports", + "data": [ + // - Without reporting + { + "id": 101, + "cfr": "FAK000999999", + "enriched": true, + "external_identification": "DONTSINK", + "flag_state": "FRA", + "imo": null, + "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "ircs": "CALLME", + "log_type": "PNO", + "operation_country": "OOF", + "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "operation_number": "FAKE_OPERATION_101", + "operation_type": "DAT", + "referenced_report_id": null, + "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "report_id": "FAKE_REPORT_101", + "software": null, + "transmission_format": "ERS", + "trip_number": 10000001, + "vessel_name": "PHENOMENE", + "trip_gears:jsonb": [ + { "gear": "GTR", "mesh": 100, "dimensions": "250;180" }, + { "gear": "GTR", "mesh": 120.5, "dimensions": "250;280" } + ], + "trip_segments:jsonb": [ + { "segment": "NWW01", "segment_name": "Chalutiers de fond" }, + { "segment": "PEL01", "segment_name": "Chalutiers pélagiques" } + ], + "value:jsonb": { + "catchOnboard": [ + { + "weight": 25.0, + "nbFish": null, + "species": "SOL", + "faoZone": "27.8.a", + "effortZone": "C", + "economicZone": "FRA", + "statisticalRectangle": "23E6" + } + ], + "pnoTypes": [ + { + "pnoTypeName": "Préavis type X", + "minimumNotificationPeriod": 4.0, + "hasDesignated_ports": false + }, + { + "pnoTypeName": "Préavis type Y", + "minimumNotificationPeriod": 8.0, + "hasDesignated_ports": true + } + ], + "port": "FRSML", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + }, + + // - With reporting + { + "id": 102, + "cfr": "ABC000042310", + "enriched": true, + "external_identification": "IW783219", + "flag_state": "FRA", + "imo": null, + "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "ircs": "QD0506", + "log_type": "PNO", + "operation_country": "OOF", + "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "operation_number": "FAKE_OPERATION_102", + "operation_type": "DAT", + "referenced_report_id": null, + "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "report_id": "FAKE_REPORT_102", + "software": null, + "transmission_format": "ERS", + "trip_number": 10000002, + "vessel_name": "COURANT MAIN PROFESSEUR", + "trip_gears:jsonb": [ + { "gear": "GTR", "mesh": 100, "dimensions": "250;180" }, + { "gear": "GTR", "mesh": 120.5, "dimensions": "250;280" } + ], + "trip_segments:jsonb": [ + { "segment": "NWW01", "segment_name": "Chalutiers de fond" }, + { "segment": "PEL01", "segment_name": "Chalutiers pélagiques" } + ], + "value:jsonb": { + "catchOnboard": [ + { + "weight": 25.0, + "nbFish": null, + "species": "SOL", + "faoZone": "27.8.a", + "effortZone": "C", + "economicZone": "FRA", + "statisticalRectangle": "23E6" + } + ], + "pnoTypes": [ + { + "pnoTypeName": "Préavis type X", + "minimumNotificationPeriod": 4.0, + "hasDesignated_ports": false + }, + { + "pnoTypeName": "Préavis type Y", + "minimumNotificationPeriod": 8.0, + "hasDesignated_ports": true + } + ], + "port": "FRSML", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + } + ] + } +] diff --git a/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql new file mode 100644 index 0000000000..186f637bad --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql @@ -0,0 +1,13 @@ +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_101'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_102'); + +INSERT INTO logbook_reports (id, cfr, enriched, external_identification, flag_state, imo, integration_datetime_utc, ircs, log_type, operation_country, operation_datetime_utc, operation_number, operation_type, referenced_report_id, report_datetime_utc, report_id, software, transmission_format, trip_number, vessel_name, trip_gears, trip_segments, value) VALUES (101, 'FAK000999999', true, 'DONTSINK', 'FRA', null, NOW() AT TIME ZONE 'UTC', 'CALLME', 'PNO', 'OOF', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_101', 'DAT', null, NOW() AT TIME ZONE 'UTC', 'FAKE_REPORT_101', null, 'ERS', 10000001, 'PHENOMENE', '[{"gear":"GTR","mesh":100,"dimensions":"250;180"},{"gear":"GTR","mesh":120.5,"dimensions":"250;280"}]', '[{"segment":"NWW01","segment_name":"Chalutiers de fond"},{"segment":"PEL01","segment_name":"Chalutiers pélagiques"}]', '{"catchOnboard":[{"weight":25,"nbFish":null,"species":"SOL","faoZone":"27.8.a","effortZone":"C","economicZone":"FRA","statisticalRectangle":"23E6"}],"pnoTypes":[{"pnoTypeName":"Préavis type X","minimumNotificationPeriod":4,"hasDesignated_ports":false},{"pnoTypeName":"Préavis type Y","minimumNotificationPeriod":8,"hasDesignated_ports":true}],"port":"FRSML","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; +UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; +UPDATE logbook_reports SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; + +INSERT INTO logbook_reports (id, cfr, enriched, external_identification, flag_state, imo, integration_datetime_utc, ircs, log_type, operation_country, operation_datetime_utc, operation_number, operation_type, referenced_report_id, report_datetime_utc, report_id, software, transmission_format, trip_number, vessel_name, trip_gears, trip_segments, value) VALUES (102, 'ABC000042310', true, 'IW783219', 'FRA', null, NOW() AT TIME ZONE 'UTC', 'QD0506', 'PNO', 'OOF', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_102', 'DAT', null, NOW() AT TIME ZONE 'UTC', 'FAKE_REPORT_102', null, 'ERS', 10000002, 'COURANT MAIN PROFESSEUR', '[{"gear":"GTR","mesh":100,"dimensions":"250;180"},{"gear":"GTR","mesh":120.5,"dimensions":"250;280"}]', '[{"segment":"NWW01","segment_name":"Chalutiers de fond"},{"segment":"PEL01","segment_name":"Chalutiers pélagiques"}]', '{"catchOnboard":[{"weight":25,"nbFish":null,"species":"SOL","faoZone":"27.8.a","effortZone":"C","economicZone":"FRA","statisticalRectangle":"23E6"}],"pnoTypes":[{"pnoTypeName":"Préavis type X","minimumNotificationPeriod":4,"hasDesignated_ports":false},{"pnoTypeName":"Préavis type Y","minimumNotificationPeriod":8,"hasDesignated_ports":true}],"port":"FRSML","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 102; +UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 102; +UPDATE logbook_reports SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 102; diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt index 22c462a862..35c2ab5fc5 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt @@ -25,7 +25,6 @@ import java.time.ZonedDateTime @ExtendWith(SpringExtension::class) class GetLogbookMessagesUTests { - @MockBean private lateinit var logbookReportRepository: LogbookReportRepository @@ -60,14 +59,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(6) @@ -142,14 +142,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(2) @@ -188,14 +189,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(3) @@ -241,16 +243,25 @@ class GetLogbookMessagesUTests { .willReturn(VoyageDatesAndTripNumber("123", ZonedDateTime.now(), ZonedDateTime.now())) given(logbookReportRepository.findAllMessagesByTripNumberBetweenDates(any(), any(), any(), any())) .willReturn( - getDummyRETLogbookMessages() + LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "", reportId = "9065646816", referencedReportId = "9065646811", operationType = LogbookOperationType.RET, messageType = "", - message = lastAck, - reportDateTime = ZonedDateTime.of(2021, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC).minusHours( - 12, + getDummyRETLogbookMessages() + + LogbookMessage( + id = 2, + analyzedByRules = listOf(), + operationNumber = "", + reportId = "9065646816", + referencedReportId = "9065646811", + operationType = LogbookOperationType.RET, + messageType = "", + message = lastAck, + reportDateTime = + ZonedDateTime.of(2021, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC).minusHours( + 12, + ), + transmissionFormat = LogbookTransmissionFormat.ERS, + integrationDateTime = ZonedDateTime.now(), + isEnriched = false, + operationDateTime = ZonedDateTime.now(), ), - transmissionFormat = LogbookTransmissionFormat.ERS, - integrationDateTime = ZonedDateTime.now(), - operationDateTime = ZonedDateTime.now(), - ), ) given(speciesRepository.find(any())).willThrow(CodeNotFoundException("not found")) given(gearRepository.find(any())).willThrow(CodeNotFoundException("not found")) @@ -258,14 +269,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(3) @@ -295,14 +307,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(3) @@ -329,14 +342,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(3) @@ -365,14 +379,15 @@ class GetLogbookMessagesUTests { given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") // When - val ersMessages = GetLogbookMessages( - logbookReportRepository, - gearRepository, - speciesRepository, - portRepository, - logbookRawMessageRepository, - ) - .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") + val ersMessages = + GetLogbookMessages( + logbookReportRepository, + gearRepository, + speciesRepository, + portRepository, + logbookRawMessageRepository, + ) + .execute("FR224226850", ZonedDateTime.now().minusMinutes(5), ZonedDateTime.now(), "345") // Then assertThat(ersMessages).hasSize(6) diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt index 86c852e4a3..0a58e2fb1f 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt @@ -116,91 +116,145 @@ object TestUtils { return listOf( LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "FAR", software = "TurboCatch (3.7-1)", + id = 2, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "FAR", + software = "TurboCatch (3.7-1)", message = far, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 1, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "DEP", software = "e-Sacapt Secours ERSV3 V 1.0.10", + id = 1, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "DEP", + software = "e-Sacapt Secours ERSV3 V 1.0.10", message = dep, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(24), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(24), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 3, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "PNO", software = "e-Sacapt Secours ERSV3 V 1.0.7", + id = 3, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "PNO", + software = "e-Sacapt Secours ERSV3 V 1.0.7", message = pno, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(0), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(0), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 3, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "COE", software = "e-Sacapt Secours ERSV3 V 1.0.7", + id = 3, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "COE", + software = "e-Sacapt Secours ERSV3 V 1.0.7", message = coe, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(3), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(3), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 4, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "COX", software = "e-Sacapt Secours ERSV3 V 1.0.7", + id = 4, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "COX", + software = "e-Sacapt Secours ERSV3 V 1.0.7", message = cox, - reportDateTime = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( - 20, - ), + reportDateTime = + ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( + 20, + ), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 5, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "CPS", software = "", + id = 5, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "CPS", + software = "", message = cpsMessage, - reportDateTime = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( - 20, - ), + reportDateTime = + ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( + 20, + ), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ) @@ -237,54 +291,81 @@ object TestUtils { return listOf( LogbookMessage( - id = 1, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "DEP", software = "FT/VISIOCaptures V1.4.7", + id = 1, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "DEP", + software = "FT/VISIOCaptures V1.4.7", message = dep, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(24), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(24), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "FAR", software = "FP/VISIOCaptures V1.4.7", + id = 2, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "FAR", + software = "FP/VISIOCaptures V1.4.7", message = far, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 3, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", operationType = LogbookOperationType.DAT, messageType = "PNO", software = "TurboCatch (3.6-1)", + id = 3, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + operationType = LogbookOperationType.DAT, + messageType = "PNO", + software = "TurboCatch (3.6-1)", message = pno, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(0), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(0), transmissionFormat = LogbookTransmissionFormat.FLUX, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ) @@ -314,37 +395,54 @@ object TestUtils { return listOf( LogbookMessage( - id = 1, analyzedByRules = listOf(), operationNumber = "9065646811", tripNumber = "345", reportId = "9065646811", operationType = LogbookOperationType.DAT, messageType = "FAR", + id = 1, + analyzedByRules = listOf(), + operationNumber = "9065646811", + tripNumber = "345", + reportId = "9065646811", + operationType = LogbookOperationType.DAT, + messageType = "FAR", message = far, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "", referencedReportId = "9065646811", operationType = LogbookOperationType.COR, messageType = "FAR", + id = 2, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "", + referencedReportId = "9065646811", + operationType = LogbookOperationType.COR, + messageType = "FAR", message = correctedFar, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ) @@ -381,111 +479,161 @@ object TestUtils { return listOf( LogbookMessage( - id = 1, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "9065646811", operationType = LogbookOperationType.DAT, messageType = "FAR", + id = 1, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "9065646811", + operationType = LogbookOperationType.DAT, + messageType = "FAR", message = far, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "", reportId = "9065646816", referencedReportId = "9065646811", operationType = LogbookOperationType.RET, messageType = "", + id = 2, + analyzedByRules = listOf(), + operationNumber = "", + reportId = "9065646816", + referencedReportId = "9065646811", + operationType = LogbookOperationType.RET, + messageType = "", message = farBadAck, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 3, analyzedByRules = listOf(), operationNumber = "", tripNumber = "345", reportId = "9065646813", operationType = LogbookOperationType.DAT, messageType = "FAR", + id = 3, + analyzedByRules = listOf(), + operationNumber = "", + tripNumber = "345", + reportId = "9065646813", + operationType = LogbookOperationType.DAT, + messageType = "FAR", message = farTwo, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 4, analyzedByRules = listOf(), operationNumber = "", reportId = "9065646818", referencedReportId = "9065646813", operationType = LogbookOperationType.RET, messageType = "", + id = 4, + analyzedByRules = listOf(), + operationNumber = "", + reportId = "9065646818", + referencedReportId = "9065646813", + operationType = LogbookOperationType.RET, + messageType = "", message = farAck, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 5, analyzedByRules = listOf(), operationNumber = "", referencedReportId = "9065646813", operationType = LogbookOperationType.DEL, messageType = "", + id = 5, + analyzedByRules = listOf(), + operationNumber = "", + referencedReportId = "9065646813", + operationType = LogbookOperationType.DEL, + messageType = "", message = farAck, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 4, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 4, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 6, analyzedByRules = listOf(), operationNumber = "5h499-erh5u7-pm3ae8c5trj78j67dfh", tripNumber = "SCR-TTT20200505030505", reportId = "zegj15-zeg56-errg569iezz3659g", operationType = LogbookOperationType.DAT, messageType = "FAR", + id = 6, + analyzedByRules = listOf(), + operationNumber = "5h499-erh5u7-pm3ae8c5trj78j67dfh", + tripNumber = "SCR-TTT20200505030505", + reportId = "zegj15-zeg56-errg569iezz3659g", + operationType = LogbookOperationType.DAT, + messageType = "FAR", message = far, - reportDateTime = ZonedDateTime.of( - 2020, - 5, - 5, - 3, - 9, - 5, - 3, - UTC, - ).minusHours(12), + reportDateTime = + ZonedDateTime.of( + 2020, + 5, + 5, + 3, + 9, + 5, + 3, + UTC, + ).minusHours(12), transmissionFormat = LogbookTransmissionFormat.FLUX, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ) } - fun getDummyPNOAndLANLogbookMessages(weightToAdd: Double = 0.0, addSpeciesToLAN: Boolean = false): List> { + fun getDummyPNOAndLANLogbookMessages( + weightToAdd: Double = 0.0, + addSpeciesToLAN: Boolean = false, + ): List> { val catchOne = Catch() catchOne.species = "TTV" catchOne.weight = 123.0 @@ -534,21 +682,23 @@ object TestUtils { // The weight is reduced because of the conversion factor // catchTwo: 788.11 = 961.5 / 1.22 // catchTwo: 51.62 = 69.7 / 1.35 - firstLan.catchLanded = listOf( - catchOne, - catchTwo.copy(weight = 788.11), - catchThree.copy(weight = 51.62), - catchFour, - catchNine, - ) + firstLan.catchLanded = + listOf( + catchOne, + catchTwo.copy(weight = 788.11), + catchThree.copy(weight = 51.62), + catchFour, + catchNine, + ) val firstPno = PNO() - firstPno.catchOnboard = listOf( - catchOne.copy(weight = catchOne.weight?.plus(weightToAdd)), - catchTwo.copy(weight = catchTwo.weight?.plus(0.5)), - catchThree.copy(weight = catchThree.weight?.plus(weightToAdd)), - catchFour, - ) + firstPno.catchOnboard = + listOf( + catchOne.copy(weight = catchOne.weight?.plus(weightToAdd)), + catchTwo.copy(weight = catchTwo.weight?.plus(0.5)), + catchThree.copy(weight = catchThree.weight?.plus(weightToAdd)), + catchFour, + ) val secondLan = LAN() if (addSpeciesToLAN) { @@ -562,36 +712,71 @@ object TestUtils { return listOf( Pair( LogbookMessage( - id = 1, analyzedByRules = listOf(), operationNumber = "456846844658", tripNumber = "125345", reportId = "456846844658", - operationType = LogbookOperationType.DAT, messageType = "LAN", message = firstLan, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 1, + analyzedByRules = listOf(), + operationNumber = "456846844658", + tripNumber = "125345", + reportId = "456846844658", + operationType = LogbookOperationType.DAT, + messageType = "LAN", + message = firstLan, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "47177857577", tripNumber = "125345", reportId = "47177857577", - operationType = LogbookOperationType.DAT, messageType = "PNO", message = firstPno, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 2, + analyzedByRules = listOf(), + operationNumber = "47177857577", + tripNumber = "125345", + reportId = "47177857577", + operationType = LogbookOperationType.DAT, + messageType = "PNO", + message = firstPno, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ), Pair( LogbookMessage( - id = 3, analyzedByRules = listOf(), operationNumber = "48545254254", tripNumber = "125345", reportId = "48545254254", - operationType = LogbookOperationType.DAT, messageType = "LAN", message = secondLan, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 3, + analyzedByRules = listOf(), + operationNumber = "48545254254", + tripNumber = "125345", + reportId = "48545254254", + operationType = LogbookOperationType.DAT, + messageType = "LAN", + message = secondLan, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 4, analyzedByRules = listOf(), operationNumber = "004045204504", tripNumber = "125345", reportId = "004045204504", - operationType = LogbookOperationType.DAT, messageType = "PNO", message = secondPno, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 4, + analyzedByRules = listOf(), + operationNumber = "004045204504", + tripNumber = "125345", + reportId = "004045204504", + operationType = LogbookOperationType.DAT, + messageType = "PNO", + message = secondPno, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ), ) } - fun getDummyPNOAndLANLogbookMessagesWithSpeciesInDouble(weightToAdd: Double = 0.0, addSpeciesToLAN: Boolean = false): List> { + fun getDummyPNOAndLANLogbookMessagesWithSpeciesInDouble( + weightToAdd: Double = 0.0, + addSpeciesToLAN: Boolean = false, + ): List> { val catchOne = Catch() catchOne.species = "TTV" catchOne.weight = 123.0 @@ -630,13 +815,14 @@ object TestUtils { firstLan.catchLanded = listOf(catchOne, catchTwo, catchTwo, catchTwo, catchThree, catchFour, catchNine) val firstPno = PNO() - firstPno.catchOnboard = listOf( - catchOne.copy(weight = catchOne.weight?.plus(weightToAdd)), - catchTwo.copy(weight = catchTwo.weight?.plus(0.5)), - catchTwo, - catchThree.copy(weight = catchThree.weight?.plus(weightToAdd)), - catchFour, - ) + firstPno.catchOnboard = + listOf( + catchOne.copy(weight = catchOne.weight?.plus(weightToAdd)), + catchTwo.copy(weight = catchTwo.weight?.plus(0.5)), + catchTwo, + catchThree.copy(weight = catchThree.weight?.plus(weightToAdd)), + catchFour, + ) val secondLan = LAN() if (addSpeciesToLAN) { @@ -650,29 +836,61 @@ object TestUtils { return listOf( Pair( LogbookMessage( - id = 1, analyzedByRules = listOf(), operationNumber = "456846844658", tripNumber = "125345", reportId = "456846844658", - operationType = LogbookOperationType.DAT, messageType = "LAN", message = firstLan, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 1, + analyzedByRules = listOf(), + operationNumber = "456846844658", + tripNumber = "125345", + reportId = "456846844658", + operationType = LogbookOperationType.DAT, + messageType = "LAN", + message = firstLan, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 2, analyzedByRules = listOf(), operationNumber = "47177857577", tripNumber = "125345", reportId = "47177857577", - operationType = LogbookOperationType.DAT, messageType = "PNO", message = firstPno, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 2, + analyzedByRules = listOf(), + operationNumber = "47177857577", + tripNumber = "125345", + reportId = "47177857577", + operationType = LogbookOperationType.DAT, + messageType = "PNO", + message = firstPno, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ), Pair( LogbookMessage( - id = 3, analyzedByRules = listOf(), operationNumber = "48545254254", tripNumber = "125345", reportId = "48545254254", - operationType = LogbookOperationType.DAT, messageType = "LAN", message = secondLan, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 3, + analyzedByRules = listOf(), + operationNumber = "48545254254", + tripNumber = "125345", + reportId = "48545254254", + operationType = LogbookOperationType.DAT, + messageType = "LAN", + message = secondLan, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), LogbookMessage( - id = 4, analyzedByRules = listOf(), operationNumber = "004045204504", tripNumber = "125345", reportId = "004045204504", - operationType = LogbookOperationType.DAT, messageType = "PNO", message = secondPno, transmissionFormat = LogbookTransmissionFormat.ERS, + id = 4, + analyzedByRules = listOf(), + operationNumber = "004045204504", + tripNumber = "125345", + reportId = "004045204504", + operationType = LogbookOperationType.DAT, + messageType = "PNO", + message = secondPno, + transmissionFormat = LogbookTransmissionFormat.ERS, integrationDateTime = ZonedDateTime.now(), + isEnriched = false, operationDateTime = ZonedDateTime.now(), ), ), diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index f1bf86f7d9..086b1a6b03 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -12,7 +12,12 @@ export const BOOLEAN_AS_OPTIONS: Array> = [ export const FIVE_MINUTES = 5 * 60 * 1000 -export const COUNTRIES_AS_OPTIONS: Option[] = Object.keys(Countries.getAlpha2Codes()).map(country => ({ - label: Countries.getName(country, 'fr'), - value: country.toLowerCase() +export const COUNTRIES_AS_ALPHA2_OPTIONS: Option[] = Object.keys(Countries.getAlpha2Codes()).map(code => ({ + label: Countries.getName(code, 'fr'), + value: code.toLowerCase() +})) + +export const COUNTRIES_AS_ALPHA3_OPTIONS: Option[] = Object.keys(Countries.getAlpha3Codes()).map(code => ({ + label: Countries.getName(code, 'fr'), + value: code })) diff --git a/frontend/src/features/Logbook/Logbook.types.ts b/frontend/src/features/Logbook/Logbook.types.ts index 3c1f3c721b..71137795c9 100644 --- a/frontend/src/features/Logbook/Logbook.types.ts +++ b/frontend/src/features/Logbook/Logbook.types.ts @@ -15,6 +15,7 @@ export type FishingActivities = { logbookMessages: LogbookMessage[] } +// TODO Replace this type with `LogbookMessage.LogbookMessage`. export type LogbookMessage = { acknowledge: { dateTime: string | null diff --git a/frontend/src/features/Logbook/LogbookMessage.types.ts b/frontend/src/features/Logbook/LogbookMessage.types.ts new file mode 100644 index 0000000000..0d8cfcf5af --- /dev/null +++ b/frontend/src/features/Logbook/LogbookMessage.types.ts @@ -0,0 +1,100 @@ +import type { Vessel } from '@features/Vessel/Vessel.types' +import type { Undefine } from '@mtes-mct/monitor-ui' + +export namespace LogbookMessage { + export type LogbookMessage = { + acknowledge: Aknowledge | undefined + deleted: boolean + externalReferenceNumber: string + flagState: string + imo: string | undefined + integrationDateTime: string + internalReferenceNumber: string + ircs: string + isCorrected: boolean + isSentByFailoverSoftware: boolean + message: Message + messageType: string + operationDateTime: string + operationNumber: string + operationType: string + rawMessage: string + referencedReportId: string | undefined + reportDateTime: string + reportId: string + tripNumber: string + vesselName: string + } + + export type Aknowledge = { + dateTime: string | undefined + isSuccess: boolean + rejectionCause: string | undefined + returnStatus: string | undefined + } + + export type Message = Undefine<{ + catchOnboard: MessageCatchonboard[] + economicZone: string + effortZone: string + faoZone: string + latitude: string + longitude: string + pnoTypes: MessagePnoType[] + /** Port code. */ + port: string + portName: string + predictedArrivalDatetimeUtc: string + predictedLandingDatetimeUtc: string + purpose: string + statisticalRectangle: string + tripStartDate: string + }> + + export type MessageCatchonboard = { + conversionFactor: number + economicZone: string + effortZone: string + faoZone: string + freshness: string + nbFish: number + packaging: string + presentation: string + preservationState: string + species: string + speciesName: string + statisticalRectangle: string + weight: number + } + + export type MessagePnoType = { + hasDesignated_ports: boolean + minimumNotificationPeriod: number + // TODO Replace that with an enum. + pnoTypeName: string + } + + export type TripGear = { + dimensions: string + /** Gear code. */ + gear: string + mesh: number + } + + export type TripSegment = { + segment: string + segmentName: string + } + + export type ApiFilter = Undefine<{ + flagStates: string[] + integratedAfter: string + integratedBefore: string + portLocodes: string[] + searchQuery: string + specyCodes: string[] + tripGearCodes: string[] + tripSegmentSegments: string[] + vesselId: Vessel.VesselId + }> +} diff --git a/frontend/src/features/PriorNotification/PriorNotification.types.ts b/frontend/src/features/PriorNotification/PriorNotification.types.ts index cf6e44e85a..786eb16e4b 100644 --- a/frontend/src/features/PriorNotification/PriorNotification.types.ts +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -1,24 +1,29 @@ -import type { SeaFrontGroup } from '../../domain/entities/seaFront/constants' +// import type { SeaFrontGroup } from '../../domain/entities/seaFront/constants' import type { Vessel } from '../../domain/entities/vessel/types' import type { Port } from '../../domain/types/port' -import type { FleetSegment } from '@features/FleetSegment/types' +import type { LogbookMessage } from '@features/Logbook/LogbookMessage.types' +import type { RiskFactor } from 'domain/entities/vessel/riskFactor/types' export namespace PriorNotification { export interface PriorNotification { - alertCount: number - estimatedTimeOfArrival: string - facade: SeaFrontGroup - fleetSegments: FleetSegment[] id: number - isSubmitted: boolean - port: Port.Port - reason: PriorNotificationReason - receivedAt: string - scheduledTimeOfLanding: string - types: PriorNotificationType[] - vessel: Vessel + logbookMessage: LogbookMessage.LogbookMessage | undefined + // TODO Real time or pre-calculated and stored? + port: Port.Port | undefined + // TODO Real time or pre-calculated and stored? + reportingsCount: number + // TODO Is it a seaFront or a seaFrontGroup? + // TODO Replace with enum. + seaFront: string | undefined + tripGears: LogbookMessage.TripGear[] + tripSegments: LogbookMessage.TripSegment[] + // TODO Real time or pre-calculated and stored? + vessel: Vessel | undefined + // TODO Real time or pre-calculated and stored? + vesselRiskFactor: RiskFactor | undefined } + // TODO Fill all the possible case. Exiting labelled enum somewhere else? export enum PriorNotificationReason { LANDING = 'LANDING' } @@ -26,6 +31,7 @@ export namespace PriorNotification { LANDING: 'Débarquement' } + // TODO Check and update with datascience values. /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/string-enum */ export enum PriorNotificationType { BASS = 'BASS', diff --git a/frontend/src/features/PriorNotification/api.ts b/frontend/src/features/PriorNotification/api.ts index a72e46339f..a9fd1fca98 100644 --- a/frontend/src/features/PriorNotification/api.ts +++ b/frontend/src/features/PriorNotification/api.ts @@ -1,15 +1,17 @@ -import { monitorenvApi } from '../../api/api' +import { getUrlOrPathWithQueryParams } from '@utils/getUrlOrPathWithQueryParams' + +import { monitorfishApi } from '../../api/api' import type { PriorNotification } from './PriorNotification.types' +import type { LogbookMessage } from '@features/Logbook/LogbookMessage.types' -// TODO Replace that (and uncomment tags). Temporarely using Fake Env API to use mappings. -export const priorNotificationApi = monitorenvApi.injectEndpoints({ +export const priorNotificationApi = monitorfishApi.injectEndpoints({ endpoints: builder => ({ - getNotices: builder.query({ - // providesTags: () => [{ type: 'Notices' }], - query: () => `/v1/prior_notifications` + getPriorNotifications: builder.query({ + providesTags: () => [{ type: 'Notices' }], + query: filter => getUrlOrPathWithQueryParams(`/prior-notifications`, filter) }) }) }) -export const { useGetNoticesQuery } = priorNotificationApi +export const { useGetPriorNotificationsQuery } = priorNotificationApi diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx index 855fc635ba..50edceb26a 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx @@ -1,4 +1,4 @@ -import { COUNTRIES_AS_OPTIONS } from '@constants/index' +import { COUNTRIES_AS_ALPHA3_OPTIONS } from '@constants/index' import { PriorNotification } from '@features/PriorNotification/PriorNotification.types' import { useGetFleetSegmentsAsOptions } from '@hooks/useGetFleetSegmentsAsOptions' import { useGetGearsAsTreeOptions } from '@hooks/useGetGearsAsTreeOptions' @@ -16,7 +16,7 @@ import { Select, Size, TextInput, - type DateRange + type DateAsStringRange } from '@mtes-mct/monitor-ui' import styled from 'styled-components' @@ -70,15 +70,11 @@ export function FilterBar() { dispatch(priorNotificationActions.setListFilterValues({ lastControlPeriod: nextLastControlPeriod })) } - const updateQuery = (nextQuery: string | undefined) => { - dispatch(priorNotificationActions.setListFilterValues({ query: nextQuery })) - } - const updatePortLocodes = (nextPortLocodes: string[] | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ portLocodes: nextPortLocodes })) } - const updateReceivedAtCustomDateRange = (nextReceivedAtCustomDateRange: DateRange | undefined) => { + const updateReceivedAtCustomDateRange = (nextReceivedAtCustomDateRange: DateAsStringRange | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ receivedAtCustomDateRange: nextReceivedAtCustomDateRange })) } @@ -86,6 +82,10 @@ export function FilterBar() { dispatch(priorNotificationActions.setListFilterValues({ receivedAtPeriod: nextReceivedAtPeriod })) } + const updateSearchQuery = (nextSearchQuery: string | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ searchQuery: nextSearchQuery })) + } + const updateSpecyCodes = (nextSpecyCodes: string[] | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ specyCodes: nextSpecyCodes })) } @@ -102,11 +102,11 @@ export function FilterBar() { isLabelHidden isTransparent label="Rechercher un navire" - name="query" - onChange={updateQuery} + name="searchQuery" + onChange={updateSearchQuery} placeholder="Rechercher un navire" size={Size.LARGE} - value={listFilterValues.query} + value={listFilterValues.searchQuery} /> @@ -117,7 +117,7 @@ export function FilterBar() { label="Nationalité" name="countryCodes" onChange={updateCountryCodes} - options={COUNTRIES_AS_OPTIONS} + options={COUNTRIES_AS_ALPHA3_OPTIONS} placeholder="Nationalité" popupWidth={240} searchable @@ -246,6 +246,7 @@ export function FilterBar() { event.stopPropagation()}> + { + setIsOpen(!isOpen) + }} + > + {vesselRiskFactor.riskFactor.toFixed(0)} + + {isOpen && ( + + + {vesselRiskFactor.impactRiskFactor} + + {/* TODO Check and implement that. */} + {/* {getImpactRiskFactorText(vesselRiskFactor.impactRiskFactor, vesselRiskFactor.hasSegments)} */} + {getImpactRiskFactorText(vesselRiskFactor.impactRiskFactor, true)} + + + + + {vesselRiskFactor.probabilityRiskFactor} + + + {getProbabilityRiskFactorText( + vesselRiskFactor.probabilityRiskFactor, + hasBeenControlledWithinPastFiveYears + )} + + + + + {vesselRiskFactor.detectabilityRiskFactor} + + {getDetectabilityRiskFactorText(vesselRiskFactor.detectabilityRiskFactor, false)} + + + )} + + ) +} + +const Box = styled.span` + position: relative; + width: auto; + + * { + user-select: none; + } +` + +const Score = styled.button<{ + $value: number +}>` + align-items: center; + background-color: ${p => getRiskFactorColor(p.$value)}; + border-radius: 1px; + color: ${p => p.theme.color.white}; + display: flex; + font-family: 'Open Sans', sans-serif; + font-size: 13px; + font-weight: 700; + height: 22px; + justify-content: center; + line-height: 1; + padding: 0 0 2px; + width: 28px; +` + +const Detail = styled.div` + background: ${p => p.theme.color.white}; + box-shadow: 0px 2px 3px ${p => p.theme.color.charcoalShadow}; + height: 72px; + line-height: 18px; + margin-left: 2px; + position: absolute; + transition: 0.2s all; +` + +const DetailRow = styled.div` + display: block; + font-size: 12px; + font-weight: 500; + margin-right: 6px; + margin: 3px; + text-align: left; +` + +const DetailScore = styled.div<{ + $value: number +}>` + background-color: ${p => getRiskFactorColor(p.$value)}; + border-radius: 1px; + color: ${p => p.theme.color.white}; + display: inline-block; + font-size: 13px; + font-weight: 500; + height: 19px; + line-height: 16px; + margin-right: 3px; + padding-top: 1px; + text-align: center; + user-select: none; + width: 26px; +` + +const DetailText = styled.span` + vertical-align: bottom; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx index 223f2a174f..6d2d6f21db 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/constants.tsx @@ -2,12 +2,14 @@ import { customDayjs, THEME, Tag, getOptionsFromLabelledEnum, TableWithSelectabl import { capitalizeFirstLetter } from '@utils/capitalizeFirstLetter' import { ButtonsGroupRow } from './ButtonsGroupRow' +import { VesselRiskFactor } from './VesselRiskFactor' import { SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' import { PriorNotification } from '../../PriorNotification.types' -import type { ColumnDef } from '@tanstack/react-table' +import type { CellContext, ColumnDef } from '@tanstack/react-table' +import type { RiskFactor } from 'domain/entities/vessel/riskFactor/types' -export const PRIOR_NOTIFICATION_TABLE_COLUMNS: Array> = [ +export const PRIOR_NOTIFICATION_TABLE_COLUMNS: Array> = [ { accessorFn: row => row.id, cell: ({ row }) => ( @@ -29,80 +31,90 @@ export const PRIOR_NOTIFICATION_TABLE_COLUMNS: Array row.estimatedTimeOfArrival, - cell: info => - customDayjs(info.getValue() as string) - .utc() - .format('DD/MM/YYYY à HH[h]mm'), + accessorFn: row => row.logbookMessage?.message?.predictedArrivalDatetimeUtc, + cell: (info: CellContext) => { + const predictedArrivalDatetimeUtc = info.getValue() + + return predictedArrivalDatetimeUtc + ? customDayjs(predictedArrivalDatetimeUtc).utc().format('DD/MM/YYYY à HH[h]mm') + : '-' + }, enableSorting: true, header: () => 'Arrivée estimée', id: 'estimatedTimeOfArrival', size: 120 }, { - accessorFn: row => row.scheduledTimeOfLanding, - cell: info => - customDayjs(info.getValue() as string) - .utc() - .format('DD/MM/YYYY à HH[h]mm'), + accessorFn: row => row.logbookMessage?.message?.predictedLandingDatetimeUtc, + cell: (info: CellContext) => { + const predictedLandingDatetimeUtc = info.getValue() + + return predictedLandingDatetimeUtc + ? customDayjs(predictedLandingDatetimeUtc).utc().format('DD/MM/YYYY à HH[h]mm') + : '-' + }, enableSorting: true, header: () => 'Débarque prévue', id: 'scheduledTimeOfLanding', size: 120 }, { - accessorFn: row => `${row.port.name} (${row.port.locode})`, - cell: info => info.getValue(), + accessorFn: row => (row.port ? `${row.port.name} (${row.port.locode})` : undefined), + cell: (info: CellContext) => info.getValue() ?? '-', enableSorting: true, header: () => "Port d'arrivée", id: 'port', size: 180 }, { - accessorFn: row => row.vessel.riskFactor.riskFactor, - cell: info => info.getValue(), + accessorFn: row => row.vesselRiskFactor, + cell: (info: CellContext) => ( + + ), enableSorting: true, header: () => 'Note', - id: 'riskScore.riskScore', + id: 'riskFactor.riskFactor', size: 50 }, { - accessorFn: row => row.vessel.vesselName, - cell: info => info.getValue(), + accessorFn: row => row.vessel?.vesselName, + cell: (info: CellContext) => info.getValue() ?? '-', enableSorting: true, header: () => 'Nom', id: 'vessel.vesselName', - size: 140 + size: 160 }, { - accessorFn: row => row.fleetSegments.map(fleetSegment => fleetSegment.segment).join('/'), - cell: info => info.getValue(), + accessorFn: row => row.tripSegments.map(tripSegment => tripSegment.segment).join('/'), + cell: (info: CellContext) => { + const segmentsAsText = info.getValue() + + return segmentsAsText.length > 0 ? segmentsAsText : '-' + }, enableSorting: true, header: () => 'Segments', id: 'fleetSegments', - size: 100 + size: 130 }, { - accessorFn: row => row.types.map(type => PriorNotification.PRIOR_NOTIFICATION_TYPE_LABEL[type]).join(', '), - cell: info => capitalizeFirstLetter(info.getValue() as string), + // accessorFn: row => row.types.map(type => PriorNotification.PRIOR_NOTIFICATION_TYPE_LABEL[type]).join(', '), + accessorFn: () => PriorNotification.PRIOR_NOTIFICATION_TYPE_LABEL.NOT_APPLICABLE, + cell: (info: CellContext) => + capitalizeFirstLetter(info.getValue() as string), enableSorting: true, header: () => 'Types de préavis', id: 'types', - size: 140 + size: 170 }, { - accessorFn: row => row.alertCount, - cell: info => { - const alertCount = info.getValue() as number - if (!alertCount) { + accessorFn: row => row.reportingsCount, + cell: (info: CellContext) => { + const alertCount = info.getValue() + if (alertCount === 0) { return null } - return ( - {`${ - info.getValue() as number - } sign.`} - ) + return {`${info.getValue()} sign.`} }, enableSorting: false, header: () => '', @@ -111,7 +123,7 @@ export const PRIOR_NOTIFICATION_TABLE_COLUMNS: Array row.id, - cell: info => , + cell: (info: CellContext) => , enableSorting: false, header: () => '', id: 'actions', @@ -123,7 +135,7 @@ export const PRIOR_NOTIFICATION_TYPES_AS_OPTIONS = getOptionsFromLabelledEnum( PriorNotification.PRIOR_NOTIFICATION_TYPE_LABEL ) -/* eslint-disable sort-keys-fix/sort-keys-fix */ +/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/string-enum */ export const SUB_MENU_LABEL: Record = { ALL: 'Vue d’ensemble', MED: 'MED', @@ -155,197 +167,20 @@ export const LAST_CONTROL_PERIOD_LABEL: Record = { export const LAST_CONTROL_PERIODS_AS_OPTIONS = getOptionsFromLabelledEnum(LAST_CONTROL_PERIOD_LABEL) export enum ReceivedAtPeriod { + AFTER_TWO_HOURS_AGO = 'AFTER_TWO_HOURS_AGO', AFTER_FOUR_HOURS_AGO = 'AFTER_FOUR_HOURS_AGO', - AFTER_HEIGTH_HOURS_AGO = 'AFTER_HEIGTH_HOURS_AGO', - AFTER_ONE_DAY_AGO = 'AFTER_ONE_DAY_AGO', + AFTER_EIGTH_HOURS_AGO = 'AFTER_EIGTH_HOURS_AGO', AFTER_TWELVE_HOURS_AGO = 'AFTER_TWELVE_HOURS_AGO', - AFTER_TWO_HOURS_AGO = 'AFTER_TWO_HOURS_AGO', + AFTER_ONE_DAY_AGO = 'AFTER_ONE_DAY_AGO', CUSTOM = 'CUSTOM' } export const RECEIVED_AT_PERIOD_LABEL: Record = { AFTER_TWO_HOURS_AGO: 'Arrivée estimée dans moins de 2h', AFTER_FOUR_HOURS_AGO: 'Arrivée estimée dans moins de 4h', - AFTER_HEIGTH_HOURS_AGO: 'Arrivée estimée dans moins de 8h', + AFTER_EIGTH_HOURS_AGO: 'Arrivée estimée dans moins de 8h', AFTER_TWELVE_HOURS_AGO: 'Arrivée estimée dans moins de 12h', AFTER_ONE_DAY_AGO: 'Arrivée estimée dans moins de 24h', CUSTOM: 'Période spécifique' } export const RECEIVED_AT_PERIODS_AS_OPTIONS = getOptionsFromLabelledEnum(RECEIVED_AT_PERIOD_LABEL) -/* eslint-enable sort-keys-fix/sort-keys-fix */ - -export const FAKE_PRIOR_NOTIFICATIONS: PriorNotification.PriorNotification[] = [ - { - alertCount: 2, - estimatedTimeOfArrival: '2024-03-01T03:00:00Z', - facade: SeaFrontGroup.MED, - fleetSegments: [ - { - bycatchSpecies: [], - faoAreas: [], - gears: [], - impactRiskFactor: 0.0, - segment: 'SEG01', - segmentName: 'Segment 1', - targetSpecies: [], - year: 2024 - } - ], - id: 1, - isSubmitted: false, - port: { - latitude: 0.0, - locode: 'POR01', - longitude: 0.0, - name: 'Port 1' - }, - reason: PriorNotification.PriorNotificationReason.LANDING, - receivedAt: '2024-03-01T02:00:00Z', - scheduledTimeOfLanding: '2024-03-01T04:00:00Z', - types: [PriorNotification.PriorNotificationType.DEEP_SEA_SPECIES], - vessel: { - beaconNumber: null, - declaredFishingGears: [], - district: '', - districtCode: '', - externalReferenceNumber: 'ABC1234', - flagState: '', - gauge: 0, - imo: '', - internalReferenceNumber: 'FR000123', - ircs: 'ABCDEF', - length: 16.89, - mmsi: '123 456 789', - navigationLicenceExpirationDate: '', - operatorEmails: [], - operatorName: '', - operatorPhones: [], - pinger: true, - power: 0, - proprietorEmails: [], - proprietorName: '', - proprietorPhones: [], - registryPort: '', - riskFactor: { - controlPriorityLevel: 0, - controlRateRiskFactor: 0, - detectabilityRiskFactor: 0, - gearOnboard: undefined, - impactRiskFactor: 0, - lastControlDatetime: '2023-01-05T19:52:00Z', - numberControlsLastFiveYears: 0, - numberControlsLastThreeYears: 0, - numberGearSeizuresLastFiveYears: 0, - numberInfractionsLastFiveYears: 0, - numberSpeciesSeizuresLastFiveYears: 0, - numberVesselSeizuresLastFiveYears: 0, - probabilityRiskFactor: 0, - riskFactor: 3.1, - segmentHighestImpact: '', - segmentHighestPriority: '', - segments: [], - speciesOnboard: undefined - }, - sailingCategory: '', - sailingType: '', - underCharter: false, - vesselEmails: [], - vesselId: 1, - vesselName: 'Vessel 1', - vesselPhones: [], - vesselType: '', - width: 0 - } - }, - { - alertCount: 2, - estimatedTimeOfArrival: '2024-03-01T03:00:00Z', - facade: SeaFrontGroup.NAMO, - fleetSegments: [ - { - bycatchSpecies: [], - faoAreas: [], - gears: [], - impactRiskFactor: 0.0, - segment: 'SEG02', - segmentName: 'Segment 2', - targetSpecies: [], - year: 2024 - }, - { - bycatchSpecies: [], - faoAreas: [], - gears: [], - impactRiskFactor: 0.0, - segment: 'SEG03', - segmentName: 'Segment 3', - targetSpecies: [], - year: 2024 - } - ], - id: 2, - isSubmitted: false, - port: { - latitude: 0.0, - locode: 'POR02', - longitude: 0.0, - name: 'Port 2' - }, - reason: PriorNotification.PriorNotificationReason.LANDING, - receivedAt: '2024-03-01T02:00:00Z', - scheduledTimeOfLanding: '2024-03-01T04:00:00Z', - types: [PriorNotification.PriorNotificationType.DEEP_SEA_SPECIES], - vessel: { - beaconNumber: null, - declaredFishingGears: [], - district: '', - districtCode: '', - externalReferenceNumber: 'DEF5678', - flagState: '', - gauge: 0, - imo: '', - internalReferenceNumber: 'FR000456', - ircs: 'GHIJK', - length: 24.6, - mmsi: '987 654 321', - navigationLicenceExpirationDate: '', - operatorEmails: [], - operatorName: '', - operatorPhones: [], - pinger: true, - power: 0, - proprietorEmails: [], - proprietorName: '', - proprietorPhones: [], - registryPort: '', - riskFactor: { - controlPriorityLevel: 0, - controlRateRiskFactor: 0, - detectabilityRiskFactor: 0, - gearOnboard: undefined, - impactRiskFactor: 0, - lastControlDatetime: '2024-02-18T14:12:00Z', - numberControlsLastFiveYears: 0, - numberControlsLastThreeYears: 0, - numberGearSeizuresLastFiveYears: 0, - numberInfractionsLastFiveYears: 0, - numberSpeciesSeizuresLastFiveYears: 0, - numberVesselSeizuresLastFiveYears: 0, - probabilityRiskFactor: 0, - riskFactor: 2.4, - segmentHighestImpact: '', - segmentHighestPriority: '', - segments: [], - speciesOnboard: undefined - }, - sailingCategory: '', - sailingType: '', - underCharter: false, - vesselEmails: [], - vesselId: 2, - vesselName: 'Vessel 2', - vesselPhones: [], - vesselType: '', - width: 0 - } - } -] +/* eslint-enable sort-keys-fix/sort-keys-fix, typescript-sort-keys/string-enum */ diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx index 1f7360e397..82a0e2aefb 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/index.tsx @@ -14,14 +14,15 @@ import { getExpandedRowModel } from '@tanstack/react-table' import { useVirtualizer } from '@tanstack/react-virtual' -import { Fragment, useCallback, useRef, useState } from 'react' +import { assertNotNullish } from '@utils/assertNotNullish' +import { Fragment, useCallback, useMemo, useRef, useState } from 'react' import styled from 'styled-components' -import { FAKE_PRIOR_NOTIFICATIONS, PRIOR_NOTIFICATION_TABLE_COLUMNS, SUB_MENUS_AS_OPTIONS } from './constants' +import { PRIOR_NOTIFICATION_TABLE_COLUMNS, SUB_MENUS_AS_OPTIONS } from './constants' import { FilterBar } from './FilterBar' -import { SEA_FRONT_GROUP_SEA_FRONTS, SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' -import { useGetNoticesQuery } from '../../api' -import { PriorNotification } from '../../PriorNotification.types' +import { getApiFilterFromListFilter } from './utils' +import { SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' +import { useGetPriorNotificationsQuery } from '../../api' import { priorNotificationActions } from '../../slice' export function PriorNotificationList() { @@ -29,8 +30,14 @@ export function PriorNotificationList() { const tableContainerRef = useRef(null) const dispatch = useMainAppDispatch() + const listFilter = useMainAppSelector(state => state.priorNotification.listFilterValues) + const apiFilter = useMemo(() => getApiFilterFromListFilter(listFilter), [listFilter]) const selectedSeaFrontGroup = useMainAppSelector(state => state.priorNotification.listFilterValues.seaFrontGroup) - const { data: priorNotifications, isError, isLoading } = useGetNoticesQuery(undefined, { pollingInterval: 60000 }) + const { + data: priorNotifications, + isError, + isLoading + } = useGetPriorNotificationsQuery(apiFilter, { pollingInterval: 60000 }) const [rowSelection, setRowSelection] = useState({}) const [sorting, setSorting] = useState([ @@ -41,16 +48,19 @@ export function PriorNotificationList() { ]) const countNoticesForSeaFrontGroup = useCallback( - (seaFrontGroup: SeaFrontGroup | 'EXTRA'): number => - FAKE_PRIOR_NOTIFICATIONS.filter(({ facade }) => { - if (seaFrontGroup === SeaFrontGroup.ALL) { - return true - } + (_seaFrontGroup: SeaFrontGroup | 'EXTRA'): number => + // TODO Calculate the sea front for each prior notification. + // return priorNotifications.filter(({ facade }) => { + // if (seaFrontGroup === SeaFrontGroup.ALL) { + // return true + // } - return facade && SEA_FRONT_GROUP_SEA_FRONTS[seaFrontGroup] - ? SEA_FRONT_GROUP_SEA_FRONTS[seaFrontGroup].includes(facade as any) - : false - }).length, + // return facade && SEA_FRONT_GROUP_SEA_FRONTS[seaFrontGroup] + // ? SEA_FRONT_GROUP_SEA_FRONTS[seaFrontGroup].includes(facade as any) + // : false + // }).length + + 0, [] ) @@ -63,7 +73,7 @@ export function PriorNotificationList() { const table = useReactTable({ columns: PRIOR_NOTIFICATION_TABLE_COLUMNS, - data: FAKE_PRIOR_NOTIFICATIONS, + data: priorNotifications ?? [], enableColumnResizing: false, enableRowSelection: true, enableSortingRemoval: false, @@ -155,6 +165,7 @@ export function PriorNotificationList() { } const priorNotification = row.original + assertNotNullish(priorNotification.logbookMessage?.message) return ( @@ -181,68 +192,80 @@ export function PriorNotificationList() {

    - PNO reçu : + PNO émis : - {customDayjs(priorNotification.receivedAt).utc().format('DD/MM/YYYY [à] hh[h]mm')} + {customDayjs(priorNotification.logbookMessage.reportDateTime) + .utc() + .format('DD/MM/YYYY [à] hh[h]mm')}

    Raison du PNO : - - {PriorNotification.PRIOR_NOTIFICATION_REASON_LABEL[priorNotification.reason]} - + {priorNotification.logbookMessage.message.purpose}

    - +

    - - {priorNotification.vessel.internalReferenceNumber} (CFR) - - - {priorNotification.vessel.ircs} (Call sign) - - - {priorNotification.vessel.externalReferenceNumber} (Marq. ext.) - - {priorNotification.vessel.mmsi} (MMSI) + {!!priorNotification.vessel?.internalReferenceNumber && ( + + {priorNotification.vessel.internalReferenceNumber} (CFR) + + )} + {!!priorNotification.vessel?.ircs && ( + + {priorNotification.vessel.ircs} (Call sign) + + )} + {!!priorNotification.vessel?.externalReferenceNumber && ( + + {priorNotification.vessel.externalReferenceNumber} (Marq. ext.) + + )} + {!!priorNotification.vessel?.mmsi && ( + {priorNotification.vessel.mmsi} (MMSI) + )}

    Taille du navire : - {priorNotification.vessel.length} + {priorNotification.vessel?.width ?? '-'}

    Dernier contrôle : - {customDayjs(priorNotification.vessel.riskFactor.lastControlDatetime) - .utc() - .format('[Le] DD/MM/YYYY')} + {priorNotification.vesselRiskFactor?.lastControlDatetime + ? customDayjs(priorNotification.vesselRiskFactor.lastControlDatetime) + .utc() + .format('[Le] DD/MM/YYYY') + : '-'}

    - + Nom du segment : - {priorNotification.fleetSegments - .map(fleetSegment => fleetSegment.segmentName) - .join(', ')} + {priorNotification.tripSegments.map(tripSegment => tripSegment.segmentName).join(', ')} - - Principales espèces à bord soumises à plan : - -
  • Baudroies (NCA) – 280 kg
  • -
  • Merlu européen (HKE) – 140 kg
  • -
  • Bar européen (BSS) – 45 kg
  • -
  • Sole commune (SOL) – 33 kg
  • -
  • Églefin (HAD) – 24 kg
  • -
    + + Principales espèces à bord : + {priorNotification.logbookMessage.message.catchOnboard ? ( + + {priorNotification.logbookMessage.message.catchOnboard.map( + ({ species, speciesName, weight }) => ( +
  • {`${speciesName} (${species}) – ${weight} kg`}
  • + ) + )} +
    + ) : ( + Non soumis + )}

    {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - Voir plus de détail + Voir plus de détail

    @@ -269,6 +292,7 @@ const TableWrapper = styled.div` const ExpandableRow = styled(TableWithSelectableRows.Td)` cursor: pointer; + user-select: none; ` const ExpandedRow = TableWithSelectableRows.BodyTr @@ -277,6 +301,7 @@ const ExpandedRowCell = styled(TableWithSelectableRows.Td).attrs(props => ({ ...props, $hasRightBorder: false }))` + padding: 8px 16px 16px; white-space: normal; > p:not(:first-child) { @@ -299,3 +324,17 @@ const ExpandedRowList = styled.ul` list-style: none; padding: 0; ` + +const Link = styled.button` + background: none; + border: none; + color: ${p => p.theme.color.slateGray}; + cursor: pointer; + padding: 0; + text-decoration: underline; + transition: color 0.2s; + + &:hover { + color: ${p => p.theme.color.gunMetal}; + } +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts b/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts index 50bd7ce165..ad48a231ee 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/types.ts @@ -1,9 +1,9 @@ import type { LastControlPeriod, ReceivedAtPeriod } from './constants' import type { SeaFrontGroup } from '../../../../domain/entities/seaFront/constants' import type { PriorNotification } from '../../PriorNotification.types' -import type { DateRange, RichBoolean, UndefineExcept } from '@mtes-mct/monitor-ui' +import type { DateAsStringRange, RichBoolean, UndefineExcept } from '@mtes-mct/monitor-ui' -export type ListFilterValues = UndefineExcept< +export type ListFilter = UndefineExcept< { countryCodes: string[] fleetSegmentSegments: string[] @@ -11,13 +11,12 @@ export type ListFilterValues = UndefineExcept< hasOneOrMoreReportings: RichBoolean isLessThanTwelveMetersVessel: RichBoolean isSent: boolean - isVesselPretargeted: boolean lastControlPeriod: LastControlPeriod portLocodes: string[] - query: string - receivedAtCustomDateRange: DateRange + receivedAtCustomDateRange: DateAsStringRange receivedAtPeriod: ReceivedAtPeriod | undefined seaFrontGroup: SeaFrontGroup | 'EXTRA' + searchQuery: string specyCodes: string[] types: PriorNotification.PriorNotificationType[] }, diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/utils.ts b/frontend/src/features/PriorNotification/components/PriorNotificationList/utils.ts new file mode 100644 index 0000000000..a22838b68b --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/utils.ts @@ -0,0 +1,74 @@ +import { customDayjs } from '@mtes-mct/monitor-ui' + +import { ReceivedAtPeriod } from './constants' + +import type { ListFilter } from './types' +import type { LogbookMessage } from '@features/Logbook/LogbookMessage.types' + +function getDateRangeFromReceivedAtPeriod(period: ReceivedAtPeriod | undefined) { + if (!period) { + return { + integratedAfter: undefined, + integratedBefore: undefined + } + } + + switch (period) { + case ReceivedAtPeriod.AFTER_TWO_HOURS_AGO: + return { + integratedAfter: customDayjs().subtract(2, 'hours').toISOString(), + integratedBefore: undefined + } + + case ReceivedAtPeriod.AFTER_FOUR_HOURS_AGO: + return { + integratedAfter: customDayjs().subtract(4, 'hours').toISOString(), + integratedBefore: undefined + } + + case ReceivedAtPeriod.AFTER_EIGTH_HOURS_AGO: + return { + integratedAfter: customDayjs().subtract(8, 'hours').toISOString(), + integratedBefore: undefined + } + + case ReceivedAtPeriod.AFTER_TWELVE_HOURS_AGO: + return { + integratedAfter: customDayjs().subtract(12, 'hours').toISOString(), + integratedBefore: undefined + } + + case ReceivedAtPeriod.AFTER_ONE_DAY_AGO: + return { + integratedAfter: customDayjs().subtract(1, 'day').toISOString(), + integratedBefore: undefined + } + + default: + return { + integratedAfter: undefined, + integratedBefore: undefined + } + } +} + +export function getApiFilterFromListFilter(listFilterValues: ListFilter): LogbookMessage.ApiFilter { + const { integratedAfter, integratedBefore } = listFilterValues.receivedAtCustomDateRange + ? { + integratedAfter: listFilterValues.receivedAtCustomDateRange[0], + integratedBefore: listFilterValues.receivedAtCustomDateRange[1] + } + : getDateRangeFromReceivedAtPeriod(listFilterValues.receivedAtPeriod) + + return { + flagStates: listFilterValues.countryCodes, + integratedAfter, + integratedBefore, + portLocodes: listFilterValues.portLocodes, + searchQuery: listFilterValues.searchQuery, + specyCodes: listFilterValues.specyCodes, + tripGearCodes: listFilterValues.gearCodes, + tripSegmentSegments: listFilterValues.fleetSegmentSegments, + vesselId: undefined + } +} diff --git a/frontend/src/features/PriorNotification/slice.ts b/frontend/src/features/PriorNotification/slice.ts index 1b2c1d6155..deee406d21 100644 --- a/frontend/src/features/PriorNotification/slice.ts +++ b/frontend/src/features/PriorNotification/slice.ts @@ -1,28 +1,28 @@ +import { RichBoolean } from '@mtes-mct/monitor-ui' import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { ReceivedAtPeriod } from './components/PriorNotificationList/constants' import { SeaFrontGroup } from '../../domain/entities/seaFront/constants' -import type { ListFilterValues } from './components/PriorNotificationList/types' +import type { ListFilter } from './components/PriorNotificationList/types' interface PriorNotificationState { - listFilterValues: ListFilterValues + listFilterValues: ListFilter } const INITIAL_STATE: PriorNotificationState = { listFilterValues: { countryCodes: undefined, fleetSegmentSegments: undefined, gearCodes: undefined, - hasOneOrMoreReportings: undefined, - isLessThanTwelveMetersVessel: undefined, + hasOneOrMoreReportings: RichBoolean.BOTH, + isLessThanTwelveMetersVessel: RichBoolean.BOTH, isSent: undefined, - isVesselPretargeted: undefined, lastControlPeriod: undefined, portLocodes: undefined, - query: undefined, receivedAtCustomDateRange: undefined, receivedAtPeriod: ReceivedAtPeriod.AFTER_FOUR_HOURS_AGO, seaFrontGroup: SeaFrontGroup.ALL, + searchQuery: undefined, specyCodes: undefined, types: undefined } @@ -32,7 +32,7 @@ const priorNotificationSlice = createSlice({ initialState: INITIAL_STATE, name: 'priorNotification', reducers: { - setListFilterValues(state, action: PayloadAction>) { + setListFilterValues(state, action: PayloadAction>) { state.listFilterValues = { ...state.listFilterValues, ...action.payload diff --git a/frontend/src/features/SideWindow/index.tsx b/frontend/src/features/SideWindow/index.tsx index 03d4ae8ead..d3fde952db 100644 --- a/frontend/src/features/SideWindow/index.tsx +++ b/frontend/src/features/SideWindow/index.tsx @@ -18,7 +18,6 @@ import styled, { createGlobalStyle, StyleSheetManager } from 'styled-components' import { Alert } from './Alert' import { BeaconMalfunctionBoard } from './BeaconMalfunctionBoard' import { Menu } from './Menu' -import { openSideWindowPath } from './useCases/openSideWindowPath' import { MissionEventContext } from '../../context/MissionEventContext' import { SideWindowMenuKey } from '../../domain/entities/sideWindow/constants' import { closeBeaconMalfunctionInKanban } from '../../domain/shared_slices/BeaconMalfunction' @@ -108,8 +107,6 @@ export function SideWindow({ isFromURL }: SideWindowProps) { dispatch(getAllCurrentReportings()) dispatch(getInfractions()) dispatch(getAllGearCodes()) - - dispatch(openSideWindowPath({ menu: SideWindowMenuKey.PRIOR_NOTIFICATION_LIST })) } dispatch(getOperationalAlerts()) diff --git a/frontend/src/features/Vessel/Vessel.types.ts b/frontend/src/features/Vessel/Vessel.types.ts new file mode 100644 index 0000000000..79632b49b7 --- /dev/null +++ b/frontend/src/features/Vessel/Vessel.types.ts @@ -0,0 +1,8 @@ +import type { VesselIdentifier } from '../../domain/entities/vessel/types' + +export namespace Vessel { + export type VesselId = { + identifier: VesselIdentifier + value: string + } +} diff --git a/frontend/src/features/VesselList/VesselListFilters.tsx b/frontend/src/features/VesselList/VesselListFilters.tsx index 8e20ce359a..a0f5324600 100644 --- a/frontend/src/features/VesselList/VesselListFilters.tsx +++ b/frontend/src/features/VesselList/VesselListFilters.tsx @@ -3,7 +3,7 @@ import { Checkbox, CheckboxGroup, MultiCascader, SelectPicker, Tag, TagPicker } import styled from 'styled-components' import { lastControlAfterLabels, lastPositionTimeAgoLabels } from './dataFormatting' -import { COUNTRIES_AS_OPTIONS } from '../../constants' +import { COUNTRIES_AS_ALPHA2_OPTIONS } from '../../constants' import { COLORS } from '../../constants/constants' import { LayerType as LayersType } from '../../domain/entities/layers/constants' import { VesselLocation, vesselSize } from '../../domain/entities/vessel/vessel' @@ -138,7 +138,7 @@ function UnmemoizedVesselListFilters({ /> `${queryParamAsString}${queryParamAsString ? '&' : ''}${key}=${valueItem}`, + '' + ) + } + + if (isObject(value)) { + return Object.entries(value).reduce((queryParamAsString, [nestedKey, nestedValue]) => { + if (nestedValue === undefined) { + return queryParamAsString + } + + return `${queryParamAsString}${queryParamAsString ? '&' : ''}${key}.${nestedKey}=${nestedValue}` + }, '') + } + + return `${key}=${value}` +} + +// TODO Add unit tests. +export function getUrlOrPathWithQueryParams(urlOrPath: string, queryParamsAsObject: AnyObject): string { + const queryParamsAsString = Object.entries(queryParamsAsObject).reduce((queryParamsAsStringAcc, [key, value]) => { + if (value === undefined || value === null) { + return queryParamsAsStringAcc + } + + const queryParamAsString = getUrlQueryParamFromObjectEntry(key, value) + + return `${queryParamsAsStringAcc}${queryParamsAsStringAcc ? '&' : ''}${queryParamAsString}` + }, '') + + return `${urlOrPath}${queryParamsAsString ? '?' : ''}${queryParamsAsString}` +} From adfddb648be30c2b442a4a4503a4ec626ba3679f Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Fri, 15 Mar 2024 03:32:06 +0100 Subject: [PATCH 30/82] Add update-test-data command in Makefile --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 62481bfd21..7160f7b025 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,6 @@ run-front: run-back: run-stubbed-apis docker compose up -d --quiet-pull --wait db - cd frontend && node ./scripts/generate_test_data_seeds.mjs cd backend && ./gradlew bootRun --args='--spring.profiles.active=local --spring.config.additional-location=$(INFRA_FOLDER)' run-back-with-monitorenv: run-monitorenv @@ -48,6 +47,9 @@ clean: docker-env check-clean-archi: cd backend/tools && ./check-clean-architecture.sh +update-test-data: + cd frontend && node ./scripts/generate_test_data_seeds.mjs + ################################################################################ # Testing From 9d16850ad76c8ff4eca45d0d332b5307e3c937bb Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Fri, 15 Mar 2024 03:32:57 +0100 Subject: [PATCH 31/82] Finalize prior notification list filters --- .../domain/entities/logbook/LogbookMessage.kt | 3 +- .../entities/logbook/LogbookTripSegment.kt | 5 +- .../monitorfish/domain/entities/port/Port.kt | 9 +- .../prior_notification/PriorNotification.kt | 36 ++++ .../PriorNotificationType.kt | 10 + .../domain/entities/vessel/VesselId.kt | 6 - .../repositories/LogbookReportRepository.kt | 6 +- .../repositories/ReportingRepository.kt | 2 +- .../GetPriorNotificationTypes.kt | 13 ++ .../GetPriorNotifications.kt | 40 ++-- .../dtos/PriorNotification.kt | 22 --- .../reporting/GetAllCurrentReportings.kt | 4 +- .../api/bff/PriorNotificationController.kt | 14 +- .../outputs/LogbookMessageCatchDataOutput.kt | 39 ++++ .../LogbookMessageTripSegmentDataOutput.kt | 8 +- .../api/outputs/PortDataOutput.kt | 23 ++- .../outputs/PriorNotificationDataOutput.kt | 79 +++++--- .../PriorNotificationTypeDataOutput.kt | 19 ++ .../database/entities/LogbookReportEntity.kt | 59 +++++- .../database/entities/PortEntity.kt | 19 +- .../database}/filters/LogbookReportFilter.kt | 12 +- .../filters/PriorNotificationFilter.kt | 2 +- .../database}/filters/ReportingFilter.kt | 3 +- .../JpaLogbookReportRepository.kt | 99 ++++++++-- .../repositories/JpaReportingRepository.kt | 40 +--- .../repositories/JpaRiskFactorsRepository.kt | 1 + .../interfaces/DBLogbookReportRepository.kt | 7 +- .../LogbookReportSpecification.kt | 17 ++ .../V0.245__Update_logbook_reports_table.sql | 3 +- ...46__Create_jsonb_contains_any_function.sql | 7 + .../testdata/V0.221.0__Insert_port_codes.sql | 2 +- ...nsert_logbook_raw_messages_and reports.sql | 174 +++++++++--------- ...5.1__Insert_more_pno_logbook_reports.jsonc | 88 +++++---- ...6.5.1__Insert_more_pno_logbook_reports.sql | 30 ++- .../JpaFacadeAreasRepositoryITests.kt | 1 - frontend/src/components/Ellipsised.tsx | 7 + frontend/src/domain/types/port.ts | 1 + .../src/features/Logbook/Logbook.types.ts | 4 +- .../features/Logbook/LogbookMessage.types.ts | 54 ++++-- frontend/src/features/Logbook/constants.ts | 1 + .../PriorNotification.types.ts | 126 +++++++------ .../src/features/PriorNotification/api.ts | 7 +- .../PriorNotificationList/FilterBar.tsx | 73 +++++--- .../VesselRiskFactor.tsx | 140 -------------- .../PriorNotificationList/constants.tsx | 85 +++++---- .../PriorNotificationList/index.tsx | 106 ++++++----- .../components/PriorNotificationList/types.ts | 12 +- .../components/PriorNotificationList/utils.ts | 157 ++++++++++++---- .../useGetPriorNotificationTypesAsOptions.ts | 29 +++ .../src/features/PriorNotification/slice.ts | 6 +- .../Vessel/components/VesselRiskFactor.tsx | 148 +++++++++++++++ .../src/hooks/useGetPortsAsTreeOptions.ts | 14 +- 52 files changed, 1167 insertions(+), 705 deletions(-) create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotificationType.kt delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypes.kt delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationTypeDataOutput.kt rename backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/{domain => infrastructure/database}/filters/LogbookReportFilter.kt (50%) rename backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/{domain => infrastructure/database}/filters/PriorNotificationFilter.kt (92%) rename backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/{domain => infrastructure/database}/filters/ReportingFilter.kt (73%) create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/specifications/LogbookReportSpecification.kt create mode 100644 frontend/src/components/Ellipsised.tsx delete mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationList/VesselRiskFactor.tsx create mode 100644 frontend/src/features/PriorNotification/hooks/useGetPriorNotificationTypesAsOptions.ts create mode 100644 frontend/src/features/Vessel/components/VesselRiskFactor.tsx diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt index 17d234f0cf..980f354277 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt @@ -13,7 +13,6 @@ data class LogbookMessage( var isCorrected: Boolean? = false, val isEnriched: Boolean, val operationType: LogbookOperationType, - // TODO What's the difference between `operationDateTime`, `integrationDateTime` and `reportDateTime`? Is it in UTC? val operationDateTime: ZonedDateTime, val internalReferenceNumber: String? = null, val externalReferenceNumber: String? = null, @@ -22,7 +21,9 @@ data class LogbookMessage( val flagState: String? = null, val imo: String? = null, val messageType: String? = null, + // Submission date of the report by the vessel val reportDateTime: ZonedDateTime? = null, + // Reception date of the report by the data center val integrationDateTime: ZonedDateTime, var acknowledge: Acknowledge? = null, var deleted: Boolean? = false, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt index f8afaa1b72..4f1a4c5683 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripSegment.kt @@ -3,7 +3,8 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook import com.fasterxml.jackson.annotation.JsonProperty data class LogbookTripSegment( - val segment: String, + @JsonProperty("segment") + val code: String, @JsonProperty("segment_name") - val segmentName: String, + val name: String, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/port/Port.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/port/Port.kt index 97ff33dfc7..02f4c25bb1 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/port/Port.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/port/Port.kt @@ -3,8 +3,9 @@ package fr.gouv.cnsp.monitorfish.domain.entities.port data class Port( val locode: String, val name: String, - val facade: String? = null, - val faoAreas: List = listOf(), - val latitude: Double? = null, - val longitude: Double? = null, + val facade: String?, + val faoAreas: List, + val latitude: Double?, + val longitude: Double?, + val region: String?, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt new file mode 100644 index 0000000000..e7cb4d8255 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt @@ -0,0 +1,36 @@ +package fr.gouv.cnsp.monitorfish.domain.entities.prior_notification + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripSegment + +data class PriorNotification( + val id: Long, + val expectedArrivalDate: String?, + val expectedLandingDate: String?, + val isVesselUnderCharter: Boolean?, + val notificationTypeLabel: String? = null, + val onboardCatches: List, + val portLocode: String?, + val portName: String? = null, + val purposeCode: String?, + val reportingsCount: Int? = null, + val seaFront: String? = null, + val sentAt: String?, + val tripGears: List, + val tripSegments: List, + val types: List, + val vesselId: Int, + val vesselExternalReferenceNumber: String?, + val vesselFlagCountryCode: String?, + val vesselInternalReferenceNumber: String?, + val vesselIrcs: String?, + val vesselLastControlDate: String?, + val vesselLength: Double?, + val vesselMmsi: String?, + val vesselName: String?, + val vesselRiskFactorImpact: Double?, + val vesselRiskFactorProbability: Double?, + val vesselRiskFactorDetectability: Double?, + val vesselRiskFactor: Double?, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotificationType.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotificationType.kt new file mode 100644 index 0000000000..43e1e1ec0d --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotificationType.kt @@ -0,0 +1,10 @@ +package fr.gouv.cnsp.monitorfish.domain.entities.prior_notification + +import com.fasterxml.jackson.annotation.JsonProperty + +data class PriorNotificationType( + val hasDesignatedPorts: Boolean, + val minimumNotificationPeriod: Double, + @JsonProperty("pnoTypeName") + val name: String, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt deleted file mode 100644 index 2738769cb4..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/vessel/VesselId.kt +++ /dev/null @@ -1,6 +0,0 @@ -package fr.gouv.cnsp.monitorfish.domain.entities.vessel - -data class VesselId( - val identifier: VesselIdentifier, - val value: String, -) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt index 6a58b3f1ff..26227bcabf 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt @@ -2,9 +2,9 @@ package fr.gouv.cnsp.monitorfish.domain.repositories import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.VoyageDatesAndTripNumber +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound -import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter -import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.LogbookReportFilter import java.time.ZonedDateTime interface LogbookReportRepository { @@ -59,6 +59,8 @@ interface LogbookReportRepository { tripNumber: String, ): VoyageDatesAndTripNumber + fun findDistinctPriorNotificationTypes(): List + // For test purpose fun deleteAll() diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt index 61c8cb0806..cc0777d8c2 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ReportingRepository.kt @@ -5,7 +5,7 @@ import fr.gouv.cnsp.monitorfish.domain.entities.reporting.InfractionSuspicion import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Observation import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Reporting import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselIdentifier -import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.ReportingFilter import java.time.ZonedDateTime interface ReportingRepository { diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypes.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypes.kt new file mode 100644 index 0000000000..88f7144167 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypes.kt @@ -0,0 +1,13 @@ +package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification + +import fr.gouv.cnsp.monitorfish.config.UseCase +import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.* + +@UseCase +class GetPriorNotificationTypes( + private val logbookReportRepository: JpaLogbookReportRepository, +) { + fun execute(): List { + return logbookReportRepository.findDistinctPriorNotificationTypes() + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt index 759e06cf9e..f2c0c0c0f2 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt @@ -1,13 +1,13 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification import fr.gouv.cnsp.monitorfish.config.UseCase +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification import fr.gouv.cnsp.monitorfish.domain.exceptions.CodeNotFoundException -import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter -import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter -import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.LogbookReportFilter import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.* import org.locationtech.jts.geom.Coordinate import org.locationtech.jts.geom.GeometryFactory +import java.time.ZonedDateTime @UseCase class GetPriorNotifications( @@ -15,7 +15,6 @@ class GetPriorNotifications( private val logbookReportRepository: JpaLogbookReportRepository, private val portRepository: JpaPortRepository, private val reportingRepository: JpaReportingRepository, - private val riskFactorRepository: JpaRiskFactorsRepository, private val vesselRepository: JpaVesselRepository, ) { fun execute(filter: LogbookReportFilter): List { @@ -40,36 +39,21 @@ class GetPriorNotifications( } } - val vessel = - vesselRepository.findVessel( - priorNotification.logbookMessage?.internalReferenceNumber, - priorNotification.logbookMessage?.externalReferenceNumber, - priorNotification.logbookMessage?.ircs, - ) + val vessel = vesselRepository.findVessel(priorNotification.vesselId) val reportingsCount = - vessel?.id.let { vesselId -> - val reportingsFilter = - ReportingFilter( - isArchived = false, - isDeleted = false, - vesselId = vesselId, - ) - - reportingRepository.findAll(reportingsFilter).count() - } - - val vesselRiskFactor = - priorNotification.logbookMessage?.internalReferenceNumber?.let { - riskFactorRepository.findVesselRiskFactors(it) - } + vessel?.id?.let { vesselId -> + reportingRepository.findCurrentAndArchivedByVesselIdEquals( + vesselId, + // TODO Fix that. + fromDate = ZonedDateTime.now().minusYears(2), + ).count() + } ?: 0 priorNotification.copy( - port = port, + portName = port?.name, reportingsCount = reportingsCount, seaFront = seaFront, - vessel = vessel, - vesselRiskFactor = vesselRiskFactor, ) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt deleted file mode 100644 index 7f9f6250f5..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/dtos/PriorNotification.kt +++ /dev/null @@ -1,22 +0,0 @@ -package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos - -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripSegment -import fr.gouv.cnsp.monitorfish.domain.entities.port.Port -import fr.gouv.cnsp.monitorfish.domain.entities.risk_factor.VesselRiskFactor -import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel - -data class PriorNotification( - val id: Long, - val logbookMessage: LogbookMessage?, - // TODO It's only used for later use case resolution and not exposed in the data outpur. Maybe find a way to remove it from the DTO? - val portLocode: String?, - val reportingsCount: Int? = null, - val tripGears: List, - val tripSegments: List, - val port: Port? = null, - val seaFront: String? = null, - val vessel: Vessel? = null, - val vesselRiskFactor: VesselRiskFactor? = null, -) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt index 5aa4ccca9e..71e6d8dbf9 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/reporting/GetAllCurrentReportings.kt @@ -6,10 +6,10 @@ import fr.gouv.cnsp.monitorfish.domain.entities.reporting.InfractionSuspicionOrO import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Reporting import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselIdentifier -import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter import fr.gouv.cnsp.monitorfish.domain.repositories.LastPositionRepository import fr.gouv.cnsp.monitorfish.domain.repositories.ReportingRepository import fr.gouv.cnsp.monitorfish.domain.use_cases.control_units.GetAllControlUnits +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.ReportingFilter import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -26,7 +26,7 @@ class GetAllCurrentReportings( ReportingFilter( isArchived = false, isDeleted = false, - types = listOf(ReportingType.INFRACTION_SUSPICION, ReportingType.ALERT), + types = listOf(ReportingType.ALERT, ReportingType.INFRACTION_SUSPICION), ) val currents = reportingRepository.findAll(filter) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt index 6606e97b5e..695bb0fb87 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt @@ -1,8 +1,9 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.bff -import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotificationTypes import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotifications import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.PriorNotificationDataOutput +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.LogbookReportFilter import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.GetMapping @@ -13,7 +14,10 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/bff/v1/prior-notifications") @Tag(name = "Prior notifications endpoints") -class PriorNotificationController(private val getPriorNotifications: GetPriorNotifications) { +class PriorNotificationController( + private val getPriorNotifications: GetPriorNotifications, + private val getPriorNotificationTypes: GetPriorNotificationTypes, +) { @GetMapping("") @Operation(summary = "Get all prior notifications") fun getAll( @@ -21,4 +25,10 @@ class PriorNotificationController(private val getPriorNotifications: GetPriorNot ): List { return getPriorNotifications.execute(filter).map { PriorNotificationDataOutput.fromPriorNotification(it) } } + + @GetMapping("/types") + @Operation(summary = "Get all prior notification types") + fun getAll(): List { + return getPriorNotificationTypes.execute() + } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt new file mode 100644 index 0000000000..a3044a0ae2 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt @@ -0,0 +1,39 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch + +class LogbookMessageCatchDataOutput( + var weight: Double?, + var numberFish: Double?, + var species: String?, + var speciesName: String?, + var faoZone: String?, + var freshness: String?, + var packaging: String?, + var effortZone: String?, + var presentation: String?, + var economicZone: String?, + var conversionFactor: Double?, + var preservationState: String?, + var statisticalRectangle: String?, +) { + companion object { + fun fromCatch(priorNotificationType: Catch): LogbookMessageCatchDataOutput { + return LogbookMessageCatchDataOutput( + weight = priorNotificationType.weight, + numberFish = priorNotificationType.numberFish, + species = priorNotificationType.species, + speciesName = priorNotificationType.speciesName, + faoZone = priorNotificationType.faoZone, + freshness = priorNotificationType.freshness, + packaging = priorNotificationType.packaging, + effortZone = priorNotificationType.effortZone, + presentation = priorNotificationType.presentation, + economicZone = priorNotificationType.economicZone, + conversionFactor = priorNotificationType.conversionFactor, + preservationState = priorNotificationType.preservationState, + statisticalRectangle = priorNotificationType.statisticalRectangle, + ) + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt index 7313d502c5..d6d467fd58 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageTripSegmentDataOutput.kt @@ -3,14 +3,14 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripSegment class LogbookMessageTripSegmentDataOutput( - val segment: String, - val segmentName: String, + val code: String, + val name: String, ) { companion object { fun fromLogbookTripSegment(logbookTripSegment: LogbookTripSegment) = LogbookMessageTripSegmentDataOutput( - segment = logbookTripSegment.segment, - segmentName = logbookTripSegment.segmentName, + code = logbookTripSegment.code, + name = logbookTripSegment.name, ) } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PortDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PortDataOutput.kt index 6ba301d1de..1c543cd9c6 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PortDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PortDataOutput.kt @@ -3,17 +3,20 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs import fr.gouv.cnsp.monitorfish.domain.entities.port.Port data class PortDataOutput( - val locode: String? = null, - val name: String? = null, - val latitude: Double? = null, - val longitude: Double? = null, + val locode: String?, + val name: String?, + val latitude: Double?, + val longitude: Double?, + val region: String?, ) { companion object { - fun fromPort(port: Port) = PortDataOutput( - locode = port.locode, - name = port.name, - latitude = port.latitude, - longitude = port.longitude, - ) + fun fromPort(port: Port) = + PortDataOutput( + locode = port.locode, + name = port.name, + latitude = port.latitude, + longitude = port.longitude, + region = port.region, + ) } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt index caa8fa6522..eca5601bd0 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt @@ -1,49 +1,78 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs -import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification class PriorNotificationDataOutput( val id: Long, - val logbookMessage: LogbookMessageDataOutput?, - val port: PortDataOutput?, - val reportingsCount: Int, + val expectedArrivalDate: String?, + val expectedLandingDate: String?, + val isVesselUnderCharter: Boolean?, + val notificationTypeLabel: String?, + val onBoardCatches: List, + val portLocode: String?, + val portName: String?, + val purposeCode: String?, + val reportingsCount: Int?, val seaFront: String?, + val sentAt: String?, val tripGears: List, val tripSegments: List, - val vessel: VesselDataOutput?, - val vesselRiskFactor: RiskFactorDataOutput?, + val types: List, + val vesselId: Int?, + val vesselExternalReferenceNumber: String?, + val vesselFlagCountryCode: String?, + val vesselInternalReferenceNumber: String?, + val vesselIrcs: String?, + val vesselLastControlDate: String?, + val vesselLength: Double?, + val vesselMmsi: String?, + val vesselName: String?, + val vesselRiskFactorImpact: Double?, + val vesselRiskFactorProbability: Double?, + val vesselRiskFactorDetectability: Double?, + val vesselRiskFactor: Double?, ) { companion object { fun fromPriorNotification(priorNotification: PriorNotification): PriorNotificationDataOutput { - val logbookMessage = - priorNotification.logbookMessage?.let { - LogbookMessageDataOutput.fromLogbookMessage( - it, - ) - } - val port = priorNotification.port?.let { PortDataOutput.fromPort(it) } - val tripGears = - priorNotification.tripGears.map { LogbookMessageTripGearDataOutput.fromLogbookTripGear(it) } + val onBoardCatches = priorNotification.onboardCatches.map { LogbookMessageCatchDataOutput.fromCatch(it) } + val tripGears = priorNotification.tripGears.map { LogbookMessageTripGearDataOutput.fromLogbookTripGear(it) } val tripSegments = - priorNotification.tripSegments.map { LogbookMessageTripSegmentDataOutput.fromLogbookTripSegment(it) } - val vessel = priorNotification.vessel?.let { VesselDataOutput.fromVessel(it) } - val vesselRiskFactor = - priorNotification.vesselRiskFactor?.let { - RiskFactorDataOutput.fromVesselRiskFactor( + priorNotification.tripSegments.map { + LogbookMessageTripSegmentDataOutput.fromLogbookTripSegment( it, ) } + val types = priorNotification.types.map { PriorNotificationTypeDataOutput.fromPriorNotificationType(it) } return PriorNotificationDataOutput( id = priorNotification.id, - logbookMessage, - port, - reportingsCount = requireNotNull(priorNotification.reportingsCount), + expectedArrivalDate = priorNotification.expectedArrivalDate, + expectedLandingDate = priorNotification.expectedLandingDate, + isVesselUnderCharter = priorNotification.isVesselUnderCharter, + notificationTypeLabel = priorNotification.notificationTypeLabel, + onBoardCatches, + portLocode = priorNotification.portLocode, + portName = priorNotification.portName, + purposeCode = priorNotification.purposeCode, + reportingsCount = priorNotification.reportingsCount, seaFront = priorNotification.seaFront, + sentAt = priorNotification.sentAt, tripGears, tripSegments, - vessel, - vesselRiskFactor, + types, + vesselId = priorNotification.vesselId, + vesselExternalReferenceNumber = priorNotification.vesselExternalReferenceNumber, + vesselFlagCountryCode = priorNotification.vesselFlagCountryCode, + vesselInternalReferenceNumber = priorNotification.vesselInternalReferenceNumber, + vesselIrcs = priorNotification.vesselIrcs, + vesselLastControlDate = priorNotification.vesselLastControlDate, + vesselLength = priorNotification.vesselLength, + vesselMmsi = priorNotification.vesselMmsi, + vesselName = priorNotification.vesselName, + vesselRiskFactorImpact = priorNotification.vesselRiskFactorImpact, + vesselRiskFactorProbability = priorNotification.vesselRiskFactorProbability, + vesselRiskFactorDetectability = priorNotification.vesselRiskFactorDetectability, + vesselRiskFactor = priorNotification.vesselRiskFactor, ) } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationTypeDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationTypeDataOutput.kt new file mode 100644 index 0000000000..3f77aa56d4 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationTypeDataOutput.kt @@ -0,0 +1,19 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotificationType + +class PriorNotificationTypeDataOutput( + val hasDesignatedPorts: Boolean, + val minimumNotificationPeriod: Double, + val name: String, +) { + companion object { + fun fromPriorNotificationType(priorNotificationType: PriorNotificationType): PriorNotificationTypeDataOutput { + return PriorNotificationTypeDataOutput( + hasDesignatedPorts = priorNotificationType.hasDesignatedPorts, + minimumNotificationPeriod = priorNotificationType.minimumNotificationPeriod, + name = priorNotificationType.name, + ) + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt index cd89084fdd..aabb56d544 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt @@ -2,8 +2,9 @@ package fr.gouv.cnsp.monitorfish.infrastructure.database.entities import com.fasterxml.jackson.databind.ObjectMapper import fr.gouv.cnsp.monitorfish.domain.entities.logbook.* +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotificationType import fr.gouv.cnsp.monitorfish.domain.mappers.ERSMapper.getERSMessageValueFromJSON -import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification import io.hypersistence.utils.hibernate.type.array.ListArrayType import io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType import io.hypersistence.utils.hibernate.type.json.JsonBinaryType @@ -73,6 +74,13 @@ data class LogbookReportEntity( @Type(JsonBinaryType::class) @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") val tripSegments: String? = null, + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "vessel_id", nullable = true) + val vessel: VesselEntity? = null, + @ManyToOne(fetch = FetchType.LAZY, optional = true) + // TODO Should it be CFR? + @JoinColumn(name = "cfr", referencedColumnName = "cfr", nullable = true, insertable = false, updatable = false) + val vesselRiskFactor: RiskFactorsEntity? = null, ) { companion object { fun fromLogbookMessage( @@ -102,8 +110,12 @@ data class LogbookReportEntity( ) } - fun toLogbookMessage(mapper: ObjectMapper) = - LogbookMessage( + fun toLogbookMessage(mapper: ObjectMapper): LogbookMessage { + val message = getERSMessageValueFromJSON(mapper, message, messageType, operationType) + val tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java) + val tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java) + + return LogbookMessage( id = id!!, internalReferenceNumber = internalReferenceNumber, referencedReportId = referencedReportId, @@ -121,27 +133,58 @@ data class LogbookReportEntity( imo = imo, messageType = messageType, analyzedByRules = analyzedByRules ?: listOf(), - message = getERSMessageValueFromJSON(mapper, message, messageType, operationType), + message = message, software = software, transmissionFormat = transmissionFormat, isEnriched = isEnriched, - tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java), - tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java), + tripGears = tripGears, + tripSegments = tripSegments, ) + } fun toPriorNotification(mapper: ObjectMapper): PriorNotification { val messageAsJsonNode = mapper.readTree(message) + + val expectedArrivalDate = messageAsJsonNode.get("predictedArrivalDatetimeUtc")?.asText() + val expectedLandingDate = messageAsJsonNode.get("predictedLandingDatetimeUtc")?.asText() + val landingCauseCode = messageAsJsonNode.get("purpose")?.asText() + val onboardCatches = + deserializeJSONList(mapper, messageAsJsonNode.get("catchOnboard")?.toString(), Catch::class.java) val portLocode = messageAsJsonNode.get("port")?.asText() - val logbookMessage = toLogbookMessage(mapper) val tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java) val tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java) + val types = + deserializeJSONList( + mapper, + messageAsJsonNode.get("pnoTypes")?.toString(), + PriorNotificationType::class.java, + ) return PriorNotification( id = id!!, - logbookMessage = logbookMessage, + expectedArrivalDate = expectedArrivalDate, + expectedLandingDate = expectedLandingDate, + isVesselUnderCharter = vessel?.underCharter, + types = types, + onboardCatches = onboardCatches, portLocode = portLocode, + purposeCode = landingCauseCode, + sentAt = reportDateTime.toString(), tripGears = tripGears, tripSegments = tripSegments, + vesselId = vessel!!.id, + vesselExternalReferenceNumber = vessel.externalReferenceNumber, + vesselFlagCountryCode = vessel.flagState, + vesselInternalReferenceNumber = vessel.internalReferenceNumber, + vesselIrcs = vessel.ircs, + vesselMmsi = vessel.mmsi, + vesselLastControlDate = vesselRiskFactor?.lastControlDatetime?.toString(), + vesselLength = vessel.length, + vesselName = vessel.vesselName, + vesselRiskFactor = vesselRiskFactor?.riskFactor, + vesselRiskFactorDetectability = vesselRiskFactor?.detectabilityRiskFactor, + vesselRiskFactorImpact = vesselRiskFactor?.impactRiskFactor, + vesselRiskFactorProbability = vesselRiskFactor?.probabilityRiskFactor, ) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/PortEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/PortEntity.kt index d01292f357..57dc7c6908 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/PortEntity.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/PortEntity.kt @@ -30,13 +30,14 @@ data class PortEntity( @Column(name = "fao_areas") val faoAreas: List? = listOf(), ) { - - fun toPort() = Port( - locode = locode, - name = portName, - facade = facade?.let { Facade.from(it).toString() }, - faoAreas = faoAreas ?: listOf(), - latitude = latitude, - longitude = longitude, - ) + fun toPort() = + Port( + locode = locode, + name = portName, + facade = facade?.let { Facade.from(it).toString() }, + faoAreas = faoAreas ?: listOf(), + latitude = latitude, + longitude = longitude, + region = region, + ) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/LogbookReportFilter.kt similarity index 50% rename from backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt rename to backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/LogbookReportFilter.kt index 980f11e1f0..95fc73a73b 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/LogbookReportFilter.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/LogbookReportFilter.kt @@ -1,15 +1,15 @@ -package fr.gouv.cnsp.monitorfish.domain.filters - -import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselId +package fr.gouv.cnsp.monitorfish.infrastructure.database.filters data class LogbookReportFilter( val flagStates: List? = null, - val integratedAfter: String? = null, - val integratedBefore: String? = null, + val isLessThanTwelveMetersVessel: Boolean? = null, + val lastControlledAfter: String? = null, + val lastControlledBefore: String? = null, val portLocodes: List? = null, val searchQuery: String? = null, val specyCodes: List? = null, val tripSegmentSegments: List? = null, val tripGearCodes: List? = null, - val vesselId: VesselId? = null, + val willArriveAfter: String? = null, + val willArriveBefore: String? = null, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/PriorNotificationFilter.kt similarity index 92% rename from backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt rename to backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/PriorNotificationFilter.kt index 3d8472e6d7..2b49483082 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/PriorNotificationFilter.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/PriorNotificationFilter.kt @@ -1,4 +1,4 @@ -package fr.gouv.cnsp.monitorfish.domain.filters +package fr.gouv.cnsp.monitorfish.infrastructure.database.filters data class PriorNotificationFilter( val countryCodes: List? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/ReportingFilter.kt similarity index 73% rename from backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt rename to backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/ReportingFilter.kt index c2bff8faa1..7df2e7e7ac 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/filters/ReportingFilter.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/filters/ReportingFilter.kt @@ -1,4 +1,4 @@ -package fr.gouv.cnsp.monitorfish.domain.filters +package fr.gouv.cnsp.monitorfish.infrastructure.database.filters import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType @@ -6,5 +6,4 @@ data class ReportingFilter( val isArchived: Boolean? = null, val isDeleted: Boolean? = null, val types: List? = null, - val vesselId: Int? = null, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt index cd235fe315..a671f120ae 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt @@ -5,23 +5,27 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTypeMapping import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType import fr.gouv.cnsp.monitorfish.domain.entities.logbook.VoyageDatesAndTripNumber +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound -import fr.gouv.cnsp.monitorfish.domain.filters.LogbookReportFilter import fr.gouv.cnsp.monitorfish.domain.repositories.LogbookReportRepository -import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.dtos.PriorNotification import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.LogbookReportEntity +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.RiskFactorsEntity +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.LogbookReportFilter import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBLogbookReportRepository import jakarta.persistence.EntityManager +import jakarta.persistence.criteria.Join import jakarta.transaction.Transactional import org.springframework.beans.factory.annotation.Autowired import org.springframework.cache.annotation.Cacheable import org.springframework.dao.EmptyResultDataAccessException import org.springframework.data.domain.PageRequest +import org.springframework.data.jpa.domain.Specification import org.springframework.data.jpa.repository.Modifying import org.springframework.stereotype.Repository import java.time.ZoneOffset.UTC import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter @Repository class JpaLogbookReportRepository( @@ -31,6 +35,44 @@ class JpaLogbookReportRepository( ) : LogbookReportRepository { private val postgresChunkSize = 5000 + companion object { + fun withIsLessThanTwelveMetersVessel( + isLessThanTwelveMetersVessel: Boolean, + ): Specification { + return Specification { root, _, criteriaBuilder -> + val vessel: Join = root.join("vessel") + + if (isLessThanTwelveMetersVessel) { + criteriaBuilder.lessThan(vessel.get("length"), 12) + } else { + criteriaBuilder.greaterThanOrEqualTo(vessel.get("length"), 12) + } + } + } + + fun withLastControlledAfter(lastControlledAfter: String): Specification { + return Specification { root, _, criteriaBuilder -> + val vesselRiskFactor: Join = root.join("vesselRiskFactor") + + criteriaBuilder.greaterThanOrEqualTo( + vesselRiskFactor.get("lastControlDatetime"), + ZonedDateTime.parse(lastControlledAfter, DateTimeFormatter.ISO_ZONED_DATE_TIME), + ) + } + } + + fun withLastControlledBefore(lastControlledBefore: String): Specification { + return Specification { root, _, criteriaBuilder -> + val vesselRiskFactor: Join = root.join("vesselRiskFactor") + + criteriaBuilder.lessThanOrEqualTo( + vesselRiskFactor.get("lastControlDatetime"), + ZonedDateTime.parse(lastControlledBefore, DateTimeFormatter.ISO_ZONED_DATE_TIME), + ) + } + } + } + override fun findAllPriorNotifications(filter: LogbookReportFilter?): List { val criteriaBuilder = entityManager.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(LogbookReportEntity::class.java) @@ -45,29 +87,58 @@ class JpaLogbookReportRepository( ), ) - filter?.let { - it.flagStates?.let { flagStates -> - predicates.add(logbookReportEntity.get("flagState").`in`(flagStates)) - } + val predictedArrivalDatetimeUtcAsTimestamp = + criteriaBuilder.function( + "jsonb_to_timestamp", + ZonedDateTime::class.java, + logbookReportEntity.get("message"), + criteriaBuilder.literal("predictedArrivalDatetimeUtc"), + ) - it.integratedAfter?.let { integratedAfter -> + filter?.let { + it.willArriveAfter?.let { willArriveAfter -> predicates.add( criteriaBuilder.greaterThanOrEqualTo( - logbookReportEntity.get("integrationDateTime"), - ZonedDateTime.parse(integratedAfter).withZoneSameInstant(UTC), + predictedArrivalDatetimeUtcAsTimestamp, + ZonedDateTime.parse(willArriveAfter).withZoneSameInstant(UTC), ), ) } - it.integratedBefore?.let { integratedBefore -> + it.willArriveBefore?.let { willArriveBefore -> predicates.add( criteriaBuilder.lessThanOrEqualTo( - logbookReportEntity.get("integrationDateTime"), - ZonedDateTime.parse(integratedBefore).withZoneSameInstant(UTC), + predictedArrivalDatetimeUtcAsTimestamp, + ZonedDateTime.parse(willArriveBefore).withZoneSameInstant(UTC), ), ) } + it.flagStates?.let { flagStates -> + predicates.add(logbookReportEntity.get("flagState").`in`(flagStates)) + } + + it.isLessThanTwelveMetersVessel?.let { isLessThanTwelveMetersVessel -> + predicates.add( + withIsLessThanTwelveMetersVessel(isLessThanTwelveMetersVessel) + .toPredicate(logbookReportEntity, criteriaQuery, criteriaBuilder), + ) + } + + it.lastControlledAfter?.let { lastControlledAfter -> + predicates.add( + withLastControlledAfter(lastControlledAfter) + .toPredicate(logbookReportEntity, criteriaQuery, criteriaBuilder), + ) + } + + it.lastControlledBefore?.let { lastControlledBefore -> + predicates.add( + withLastControlledBefore(lastControlledBefore) + .toPredicate(logbookReportEntity, criteriaQuery, criteriaBuilder), + ) + } + it.portLocodes?.let { portLocodes -> predicates.add( criteriaBuilder.function( @@ -341,6 +412,10 @@ class JpaLogbookReportRepository( } } + override fun findDistinctPriorNotificationTypes(): List { + return dbERSRepository.findDistinctPriorNotificationType() + } + override fun updateLogbookMessagesAsProcessedByRule( ids: List, ruleType: String, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt index af9e6d8051..78723b3834 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepository.kt @@ -7,9 +7,9 @@ import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Observation import fr.gouv.cnsp.monitorfish.domain.entities.reporting.Reporting import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType import fr.gouv.cnsp.monitorfish.domain.entities.vessel.VesselIdentifier -import fr.gouv.cnsp.monitorfish.domain.filters.ReportingFilter import fr.gouv.cnsp.monitorfish.domain.repositories.ReportingRepository import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ReportingEntity +import fr.gouv.cnsp.monitorfish.infrastructure.database.filters.ReportingFilter import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBReportingRepository import jakarta.persistence.EntityManager import jakarta.transaction.Transactional @@ -80,44 +80,6 @@ class JpaReportingRepository( it.types?.let { types -> predicates.add(reportingEntity.get("type").`in`(*types.toTypedArray())) } - - it.vesselId?.let { vesselId -> - predicates.add( - criteriaBuilder.equal( - reportingEntity.get("vesselId"), - vesselId, - ), - ) - } - -// it.vesselId?.let { vesselId -> -// when (vesselId.identifier) { -// VesselIdentifier.INTERNAL_REFERENCE_NUMBER -> { -// predicates.add( -// criteriaBuilder.equal( -// reportingEntity.get("internalReferenceNumber"), -// vesselId.toString(), -// ), -// ) -// } -// VesselIdentifier.IRCS -> { -// predicates.add( -// criteriaBuilder.equal( -// reportingEntity.get("ircs"), -// vesselId.toString(), -// ), -// ) -// } -// VesselIdentifier.EXTERNAL_REFERENCE_NUMBER -> { -// predicates.add( -// criteriaBuilder.equal( -// reportingEntity.get("externalReferenceNumber"), -// vesselId.toString(), -// ), -// ) -// } -// } -// } } criteriaQuery.select(reportingEntity).where(*predicates.toTypedArray()) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaRiskFactorsRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaRiskFactorsRepository.kt index 5d9f68cb03..d845d4bd50 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaRiskFactorsRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaRiskFactorsRepository.kt @@ -17,6 +17,7 @@ class JpaRiskFactorsRepository( ) : RiskFactorsRepository { private val logger: Logger = LoggerFactory.getLogger(JpaRiskFactorsRepository::class.java) + // TODO Why don't we use the `vesseId` (=> CFR) parameter since it's a safer unique value? @Cacheable(value = ["risk_factors"]) override fun findVesselRiskFactors(internalReferenceNumber: String): VesselRiskFactor? { try { diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt index b6402a720b..d1c196790d 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt @@ -185,12 +185,11 @@ interface DBLogbookReportRepository : @Query( """ - SELECT * + SELECT DISTINCT jsonb_array_elements(value->'pnoTypes')->>'pnoTypeName' AS uniquePnoTypeName FROM logbook_reports - WHERE log_type = 'PNO' - ORDER BY operation_datetime_utc DESC + ORDER BY uniquePnoTypeName """, nativeQuery = true, ) - fun findPNOMessages(): List + fun findDistinctPriorNotificationType(): List } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/specifications/LogbookReportSpecification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/specifications/LogbookReportSpecification.kt new file mode 100644 index 0000000000..3cc9aac7b9 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/specifications/LogbookReportSpecification.kt @@ -0,0 +1,17 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.specifications + +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.LogbookReportEntity +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.VesselEntity +import jakarta.persistence.criteria.Join +import org.springframework.data.jpa.domain.Specification + +class LogbookReportSpecification { + companion object { + fun withVesselLengthGreaterThan(minLength: Double): Specification { + return Specification { root, query, criteriaBuilder -> + val vesselJoin: Join = root.join("vessel") + criteriaBuilder.greaterThan(vesselJoin.get("length"), minLength) + } + } + } +} diff --git a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql index c5fda1fddb..e7c79d9795 100644 --- a/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql +++ b/backend/src/main/resources/db/migration/internal/V0.245__Update_logbook_reports_table.sql @@ -1,4 +1,5 @@ ALTER TABLE public.logbook_reports ADD COLUMN enriched BOOLEAN NOT NULL DEFAULT false, ADD COLUMN trip_gears jsonb, - ADD COLUMN trip_segments jsonb; + ADD COLUMN trip_segments jsonb, + ADD COLUMN vessel_id INTEGER; diff --git a/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql b/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql index e7a2b10b57..ec4d172d03 100644 --- a/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql +++ b/backend/src/main/resources/db/migration/internal/V0.246__Create_jsonb_contains_any_function.sql @@ -46,3 +46,10 @@ RETURNS boolean AS $$ END; $$ LANGUAGE plpgsql IMMUTABLE; +CREATE OR REPLACE FUNCTION jsonb_to_timestamp(json_data jsonb, key text) +RETURNS timestamp AS $$ + BEGIN + RETURN (SELECT TO_TIMESTAMP(json_data ->> key, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')); + END; +$$ LANGUAGE plpgsql IMMUTABLE; + diff --git a/backend/src/main/resources/db/testdata/V0.221.0__Insert_port_codes.sql b/backend/src/main/resources/db/testdata/V0.221.0__Insert_port_codes.sql index 3b6cbafb74..91d7b71a60 100644 --- a/backend/src/main/resources/db/testdata/V0.221.0__Insert_port_codes.sql +++ b/backend/src/main/resources/db/testdata/V0.221.0__Insert_port_codes.sql @@ -20,4 +20,4 @@ values ('AD', null, null, 'ADALV', 'Andorra la Vella', 42.5, 1.016666666 ('AE', null, 'DU', 'AEHZP', 'Hamriya Free Zone Port', 25.3, 55.0833333333333, null, true), ('AE', null, 'DU', 'AEHSN', 'Hassyan', 24.9, 54.0666666666667, null, false), ('AE', null, null, 'AEHTL', 'Hulaylah Terminal', 25.9833333333333, 55.0833333333333, null, true), - ('FR', null, null, 'FRSML', 'Saint-Malo', 48.3833333333333, 00.2033333333333, null, true); + ('FR', null, '35', 'FRSML', 'Saint-Malo', 48.3833333333333, 00.2033333333333, null, true); diff --git a/backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql b/backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql index 866f31cec8..e536f37508 100644 --- a/backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql +++ b/backend/src/main/resources/db/testdata/V666.5.0__Insert_logbook_raw_messages_and reports.sql @@ -249,175 +249,175 @@ INSERT INTO logbook_reports (operation_number, analyzed_by_rules, trip_number, o operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, - cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, value, - integration_datetime_utc, transmission_format) + cfr, ircs, external_identification, vessel_id, vessel_name, flag_state, imo, log_type, + value, integration_datetime_utc, transmission_format) VALUES ('OOF20190430056936', '{"PNO_LAN_WEIGHT_TOLERANCE"}'::text[], 9463710, 'OOF', '2018-08-23T12:41:00Z', 'DAT', 'OOF20190430056936', null, '2018-08-23T12:41:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'LAN', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'LAN', '{"port": "AEJAZ", "catchLanded": [{"weight": 40.0, "nbFish": null, "species": "SCR", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}], "landingDatetimeUtc": "2018-09-03T12:18Z"}', '2021-01-18T07:17:31.532639Z', 'ERS'), ('OOF20190430056397', null, null, 'OOF', '2018-10-17T11:36:00Z', 'RET', null, 'OOF20190430056936', '2018-10-30T11:32:00Z', - null, null, null, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS'), + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS'), ('OOF99190430056936', null, 9463710, 'OOF', '2018-08-30T12:41:00Z', 'DAT', 'OOF99190430056936', null, '2018-01-30T12:41:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'LAN', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'LAN', '{"port": "AEJAZ", "catchLanded": [{"weight": 40.0, "nbFish": null, "species": "SCR", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}], "landingDatetimeUtc": "2018-09-03T12:18Z"}', '2021-01-18T07:17:31.532639Z', 'ERS'), ('OOF91190430056936', null, null, 'OOF', '2018-10-17T11:36:00Z', 'RET', null, 'OOF99190430056936', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', '{"returnStatus": "002"}', '2021-01-18T07:19:28.384921Z', 'ERS'); + null, null, null, -1, null, null, null, '', '{"returnStatus": "002"}', '2021-01-18T07:19:28.384921Z', 'ERS'); INSERT INTO logbook_reports (operation_number, trip_number, operation_country, operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, - cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, value, - integration_datetime_utc, transmission_format, software) + cfr, ircs, external_identification, vessel_id, vessel_name, flag_state, imo, log_type, + value, integration_datetime_utc, transmission_format, software) VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF20180212345698', null, '2018-02-17T01:05:00Z', - 'UNKNOWN_VESS', 'GMRS', 'ABC658', 'Unknown vessel', 'FRA', null, 'DEP', + 'UNKNOWN_VESS', 'GMRS', 'ABC658', -1, 'Unknown vessel', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2018-02-17T00:00Z", "departureDatetimeUtc": "2018-02-17T01:05Z"}', '2019-01-18T07:17:28.317559Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190227050001', 9463711, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF20180212345698', null, '2018-02-17T01:05:00Z', - 'U_W0NTFINDME', 'QGDF', 'ABC123456', 'MALOTRU', 'FRA', null, 'DEP', + 'U_W0NTFINDME', 'QGDF', 'ABC123456', 2, 'MALOTRU', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2018-02-17T00:00Z", "departureDatetimeUtc": "2018-02-17T01:05Z"}', '2019-01-18T07:17:28.317559Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190227050000', 9463711, 'OOF', '2018-02-20T01:05:00Z', 'DAT', 'OOF20180227059999', null, '2018-02-20T01:05:00Z', - 'U_W0NTFINDME', 'QGDF', 'ABC123456', 'MALOTRU', 'FRA', null, 'PNO', + 'U_W0NTFINDME', 'QGDF', 'ABC123456', 2, 'MALOTRU', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 25.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2018-02-20T00:00Z", "predictedArrivalDatetimeUtc": "2018-02-20T13:38Z"}', '2019-01-20T07:17:28.317559Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190227050002', 9463711, 'OOF', '2018-02-20T12:41:00Z', 'COR', 'OOF20180227051234', 'OOF20180227059999', '2018-02-20T12:41:00Z', - 'U_W0NTFINDME', 'QGDF', 'ABC123456', 'MALOTRU', 'FRA', null, 'PNO', + 'U_W0NTFINDME', 'QGDF', 'ABC123456', 2, 'MALOTRU', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 36.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2018-02-20T00:00Z", "predictedArrivalDatetimeUtc": "2018-02-20T13:45Z"}', '2021-01-18T07:17:31.532639Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190126036598', 9463711, 'OOF', '2019-01-18T11:45:00Z', 'DAT', 'OOF20190126036598', null, '2019-01-18T11:45:00Z', - 'U_W0NTFINDME', 'QGDF', 'ABC123456', 'MALOTRU', 'FRA', null, 'FAR', + 'U_W0NTFINDME', 'QGDF', 'ABC123456', 2, 'MALOTRU', 'FRA', null, 'FAR', '{"hauls": [{"gear": "GTN", "mesh": 100.0, "catches": [], "farDatetimeUtc": "2019-01-15T11:45:00Z"}]}', '2019-01-15T11:45:00Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190126036598', 9463711, 'OOF', '2019-01-18T11:45:00Z', 'DAT', 'OOF20190126036598', null, '2019-01-18T11:45:00Z', - 'U_W0NTFINDME', 'QGDF', 'ABC123456', 'MALOTRU', 'FRA', null, 'DIS', + 'U_W0NTFINDME', 'QGDF', 'ABC123456', 2, 'MALOTRU', 'FRA', null, 'DIS', '{"catches": [{"weight": 5.0, "nbFish": 1.0, "species": "NEP", "faoZone": "27.8.a", "freshness": null, "packaging": "BOX", "effort_zone": null, "presentation": "DIM", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "24E5"}, {"weight": 3.0, "nbFish": 2.0, "species": "BIB", "faoZone": "27.8.a", "freshness": null, "packaging": "BOX", "effortZone": null, "presentation": "DIM", "economicZone": "FRA", "preservationState": "FRE", "statisticalRectangle": "24E5"}], "discardDatetimeUtc": "2019-10-17T11:45:00Z"}', '2019-01-15T11:45:00Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190227050003', null, 'OOF', '2018-02-20T11:36:00Z', 'RET', null, 'OOF20180227051234', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2019-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190126059903', 9463713, 'OOF', '2019-01-18T11:45:00Z', 'DAT', 'OOF20190126059903', null, '2019-01-18T11:45:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'FAR', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'FAR', '{"hauls": [{"gear": "GTN", "mesh": 100.0, "catches": [], "farDatetimeUtc": "2019-01-17T12:35Z"}]}', '2021-01-18T07:17:29.361198Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190223059904', 9463713, 'OOF', '2019-02-23T13:08:00Z', 'DAT', 'OOF20190223059904', null, '2019-01-23T13:08:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-01-23T13:08Z"}', '2021-01-18T07:17:18.130726Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190227059999', 9463714, 'OOF', '2019-02-17T01:05:00Z', 'DAT', 'OOF20190227059999', null, '2019-02-17T01:05:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'DEP', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2019-02-17T00:00Z", "departureDatetimeUtc": "2019-02-17T01:05Z"}', '2021-01-18T07:17:28.317559Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190227059900', 9463714, 'OOF', '2019-02-27T01:05:00Z', 'DAT', 'OOF20190227059900', null, '2019-02-27T01:05:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'DEP', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2019-02-27T00:00Z", "departureDatetimeUtc": "2019-02-27T01:05Z"}', '2021-01-18T07:17:28.317559Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190403059904', 9463714, 'OOF', '2019-04-03T10:15:00Z', 'DAT', 'OOF20190403059904', null, '2019-04-03T10:15:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-04-03T10:15Z"}', '2021-01-18T07:17:29.092355Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190430059907', 9463714, 'OOF', '2019-04-30T12:41:00Z', 'DAT', 'OOF20190430059907', null, '2019-04-30T12:41:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'LAN', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'LAN', '{"port": "AEJAZ", "catchLanded": [{"weight": 40.0, "nbFish": null, "species": "SCR", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}], "landingDatetimeUtc": "2019-09-03T12:18Z"}', '2021-01-18T07:17:31.532639Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190430059918', 9463714, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF20190430059907', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF69850430059918', 9463714, 'OOF', '2019-04-30T12:41:00Z', 'COR', 'OOF69850430059918', 'OOF20190430059907', '2019-04-30T12:41:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'LAN', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'LAN', '{"port": "AEJAZ", "catchLanded": [{"weight": 42.0, "nbFish": null, "species": "SCR", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}], "landingDatetimeUtc": "2019-09-03T12:18Z"}', '2021-01-18T07:17:31.532639Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190433689918', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF69850430059918', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190618059902', 9463714, 'OOF', '2019-06-18T11:39:00Z', 'DAT', 'OOF20190618059902', null, '2019-06-18T11:39:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 25.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-06-18T00:00Z", "predictedArrivalDatetimeUtc": "2019-06-18T13:38Z"}', '2021-01-18T07:17:26.691341Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22123048321388', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF20190618059902', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190619059905', 9463714, 'OOF', '2019-06-19T12:49:00Z', 'DAT', 'OOF20190619059905', null, '2019-06-19T12:49:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'COX', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'COX', '{"faoZoneExited": "27.8.a", "latitudeExited": 46.488, "longitudeExited": -1.851, "effortZoneExited": "C", "economicZoneExited": "FRA", "targetSpeciesOnExit": "DEM", "effortZoneExitDatetimeUtc": "2020-08-09T13:47:00Z", "statisticalRectangleExited": "21E8"}', '2021-01-18T07:17:27.957604Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190627059906', 9463714, 'OOF', '2019-06-27T10:32:00Z', 'DAT', 'OOF20190627059906', null, '2019-06-27T10:32:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'COX', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'COX', '{"faoZoneExited": "27.8.a", "latitudeExited": 46.488, "longitudeExited": -1.851, "effortZoneExited": "C", "economicZoneExited": "FRA", "targetSpeciesOnExit": "DEM", "effortZoneExitDatetimeUtc": "2020-08-09T13:47:00Z", "statisticalRectangleExited": "21E8"}', '2021-01-18T07:17:18.844684Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190710059906', 9463714, 'OOF', '2019-07-10T11:41:00Z', 'DAT', 'OOF20190710059906', null, '2019-07-10T11:41:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'COX', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'COX', '{"faoZoneExited": "27.8.a", "latitudeExited": 46.488, "longitudeExited": -1.851, "effortZoneExited": "C", "economicZoneExited": "FRA", "targetSpeciesOnExit": "DEM", "effortZoneExitDatetimeUtc": "2020-08-09T13:47:00Z", "statisticalRectangleExited": "21E8"}', '2021-01-18T07:17:29.100464Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190722059907', 9463714, 'OOF', '2019-07-22T11:53:00Z', 'DAT', 'OOF20190722059907', null, '2019-07-22T11:53:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'RTP', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'RTP', '{"port": "AEAJM", "gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2019-07-22T11:53Z"}', '2021-01-18T07:17:27.473313Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191205059902', 9463714, 'OOF', '2019-10-15T12:01:00Z', 'DAT', 'OOF20191205059902', null, '2019-10-15T12:01:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'FAR', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'FAR', '{"hauls": [{"gear": "GTN", "mesh": 100.0, "catches": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 12.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.b", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E7"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1.5, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}],"farDatetimeUtc": "2019-12-05T11:55Z"}]}', '2021-01-18T07:17:16.627684Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22103048321388', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF20190722059907', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191011059900', 9463715, 'OOF', '2019-10-11T02:06:00Z', 'DAT', 'OOF20191011059900', null, '2019-10-11T02:06:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'DEP', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTN", "mesh": 100.0}, {"gear": "GTN", "mesh": 85.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2019-10-11T00:00Z", "departureDatetimeUtc": "2019-10-11T01:40Z"}', '2021-01-18T07:17:28.888437Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191015059904', 9463715, 'OOF', '2019-10-15T11:23:00Z', 'DAT', 'OOF20191015059904', null, '2019-10-15T11:23:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'COX', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'COX', '{"faoZoneExited": "27.8.a", "latitudeExited": 46.488, "longitudeExited": -1.851, "effortZoneExited": "C", "economicZoneExited": "FRA", "targetSpeciesOnExit": "DEM", "effortZoneExitDatetimeUtc": "2020-08-09T13:47:00Z", "statisticalRectangleExited": "21E8"}', '2021-01-18T07:17:18.98256Z', 'ERS', 'e-Sacapt Secours ERSV3 V 1.0.10'), ('OOF20190617059901', 9463715, 'OOF', '2019-10-17T01:32:00Z', 'DAT', 'OOF20190617059901', null, '2019-10-17T01:32:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'COE', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'COE', '{"faoZoneEntered": "27.8.a", "latitudeEnetered": 46.695, "effortZoneEntered": "C", "longitudeEnetered": -1.943, "economicZoneEntered": "FRA", "targetSpeciesOnEntry": "PEL", "effortZoneEntryDatetimeUtc": "2020-08-10T03:16:00Z", "statisticalRectangleEntered": "22E8"}', '2021-01-18T07:17:18.324128Z', 'ERS', 'FP/VISIOCaptures V1.4.7'), ('OOF20190617056738', 9463715, 'OOF', '2019-10-17T01:32:00Z', 'DAT', 'OOF20190617056738', null, '2019-10-17T01:32:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'CRO', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'CRO', '{"faoZoneEntered": "27.8.a", "latitudeEntered": 46.695, "effortZoneEntered": "C", "longitudeEntered": -1.943, "economicZoneEntered": "FRA", "targetSpeciesOnEntry": "PEL", "effortZoneEntryDatetimeUtc": "2020-08-10T03:16:00Z", "statisticalRectangleEntered": "22E8", "faoZoneExited": "27.7.d", "latitudeExited": 49.629, "longitudeExited": -0.899, "effortZoneExited": "B", "economicZoneExited": "FRA", "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-11-04T20:50:00Z", "statisticalRectangleExited": "28E9"}', '2021-01-18T07:17:18.324128Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191030059902', 9463715, 'OOF', '2019-10-17T11:32:00Z', 'DAT', 'OOF20191030059902', null, '2019-10-30T11:32:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'FAR', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'FAR', '{"hauls": [{"gear": "GTN", "mesh": 100.0, "dimensions": "150.0;120.0", "catches": [{"weight": 1500.0, "conversionFactor": 1.2, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 86.0, "nbFish": null, "conversionFactor": 1.0, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 17.0, "nbFish": null, "species": "RJH", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "farDatetimeUtc": "2019-10-17T11:32:00Z"}]}', '2021-01-18T07:17:27.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20103048326985', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', 'OOF20103048326985', 'OOF20191030059902', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191030059903', 9463715, 'OOF', '2019-10-17T11:36:00Z', 'COR', 'OOF20191030059903', 'OOF20191030059902', '2019-10-30T11:32:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'FAR', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'FAR', -- As a result of a change in the ers parser in december 2023, the `dimensions` field holds numeric data as well as string data. -- Before the change, the parser casted the `dimensions` field extracted from xml files to float -- But this field can contain strings that cannot be casted to float, like "150.0;120.0" for gears with multiple dimensions. @@ -429,172 +429,172 @@ VALUES ('OOF20190265896325', 9463701, 'OOF', '2018-02-17T01:05:00Z', 'DAT', 'OOF '2021-01-18T07:19:27.384921Z', 'ERS', 'e-Sacapt Secours ERSV3 V 1.0.7'), ('OOF20191030056523', 9463715, 'OOF', '2019-10-17T11:37:00Z', 'DAT', 'OOF20191030056523', 'OOF20191030056523', '2019-10-30T11:37:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'INS', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'INS', 'null', '2021-01-18T07:19:27.384921Z', 'ERS', 'JP/VISIOCaptures V1.4.7'), ('OOF20103048321388', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', 'OOF19103048321388', 'OOF20191030059903', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191030059909', 9463715, 'OOF', '2019-10-17T11:45:00Z', 'DAT', 'OOF20191030059909', null, '2019-10-30T11:38:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'DIS', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'DIS', '{"catches": [{"weight": 5.0, "nbFish": 1.0, "species": "NEP", "faoZone": "27.8.a", "freshness": null, "packaging": "BOX", "effort_zone": null, "presentation": "DIM", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "24E5"}, {"weight": 3.0, "nbFish": 2.0, "species": "BIB", "faoZone": "27.8.a", "freshness": null, "packaging": "BOX", "effortZone": null, "presentation": "DIM", "economicZone": "FRA", "preservationState": "FRE", "statisticalRectangle": "24E5"}], "discardDatetimeUtc": "2019-10-17T11:45:00Z"}', '2021-01-18T07:17:27.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22103048326325', null, 'OOF', '2021-01-18T07:19:29.384921Z', 'RET', null, 'OOF20191030059909', '2021-01-18T07:19:29.384921Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:29.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191203059903', 9463715, 'OOF', '2019-10-20T12:16:00Z', 'DAT', 'OOF20191203059903', null, '2019-12-03T12:16:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'EOF', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'EOF', '{"endOfFishingDatetimeUtc": "2019-10-20T12:16:00Z"}', '2021-01-18T07:17:26.736456Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20191011059902', 9463715, 'OOF', '2019-10-21T08:16:00Z', 'DAT', 'OOF20191011059902', null, '2019-10-11T08:16:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'PNO', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 20.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 153.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "catchToLand": [{"weight": 15.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 151.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2019-10-11T00:00Z", "predictedArrivalDatetimeUtc": "2019-10-21T08:16:00Z"}', '2021-01-18T07:17:19.04244Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22113048321388', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF20191011059902', - '2019-10-30T11:32:00Z', null, null, null, null, null, null, '', + '2019-10-30T11:32:00Z', null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190830059906', 9463715, 'OOF', '2019-10-21T11:12:00Z', 'DAT', 'OOF20190830059906', null, '2019-08-30T11:12:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'RTP', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'RTP', '{"port": "AEAJM", "gearOnboard": [{"gear": "GTN", "mesh": 100.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2019-10-21T11:12:00Z"}', '2021-01-18T07:17:20.007244Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190830059966', null, 'OOF', '2019-10-21T11:12:00Z', 'RET', 'OOF20190830059966', 'OOF20190830059906', '2019-08-30T11:12:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "002", "rejectionCause": "002 MGEN02 Message incorrect : la date/heure de l’événement RTP n° OOF20201105037001 est postérieure à la date/heure courante. Veuillez vérifier la date/heure de l’événement déclaré et renvoyer votre message."}', '2021-01-18T07:17:21.007244Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190627059908', 9463715, 'OOF', '2019-10-22T11:06:00Z', 'DAT', 'OOF20190627059908', null, '2019-06-27T11:06:00Z', - 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'LAN', + 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'LAN', '{"port": "AEAJM", "sender": "MAS", "catchLanded": [{"weight": 10.0, "conversionFactor": 1.2, "nbFish": null, "species": "SLS", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 20.0, "conversionFactor": 1.0, "nbFish": null, "species": "SLS", "faoZone": "27.8.b", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 180.0, "nbFish": null, "species": "HKC", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 1500.0, "nbFish": null, "species": "BON", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}, {"weight": 200.0, "nbFish": null, "species": "SCR", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "24E6"}, {"weight": 6.0, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "24E6"}], "landingDatetimeUtc": "2019-10-22T11:06:00Z"}', '2021-01-18T09:17:28.2717Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20103048323658', 9463715, 'OOF', '2019-10-11T01:06:00Z', 'DAT', 'OOF20103048323658', null, - '2019-10-11T01:06:00Z', 'FAK000999999', 'CALLME', 'DONTSINK', 'PHENOMENE', 'FRA', null, 'CPS', + '2019-10-11T01:06:00Z', 'FAK000999999', 'CALLME', 'DONTSINK', 1, 'PHENOMENE', 'FRA', null, 'CPS', '{"cpsDatetimeUtc": "2023-02-28T17:44:00Z","gear": "GTR","mesh": 100.0,"dimensions": "50.0;2.0","catches": [{"sex": "M","healthState": "DEA","careMinutes": null,"ring": "1234567","fate": "DIS","comment": null,"species": "DCO","weight": 60.0,"nbFish": 1.0,"faoZone": "27.8.a","economicZone": "FRA","statisticalRectangle": "22E7","effortZone": "C"},{"sex": "M","healthState": "DEA","careMinutes": 40,"ring": "1234568","fate": "DIS","comment": "A comment","species": "DCO","weight": 80.0,"nbFish": 1.0,"faoZone": "27.8.a","economicZone": "FRA","statisticalRectangle": "22E7","effortZone": "C"}],"latitude": 46.575,"longitude": -2.741}', '2019-10-11T01:17:28.2717Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22103048321399', null, 'OOF', '2019-10-17T11:36:00Z', 'RET', null, 'OOF20190627059908', '2019-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22103048321399', null, 'OOF', '2019-10-17T11:36:00Z', 'DEL', null, 'OOF20190627059908', - '2019-10-30T11:32:00Z', null, null, null, null, null, null, '', null, '2021-01-18T07:19:28.384921Z', 'ERS', + '2019-10-30T11:32:00Z', null, null, null, -1, null, null, null, '', null, '2021-01-18T07:19:28.384921Z', 'ERS', 'TurboCatch (3.7-1)'), ('OOF22103048321398', null, 'OOF', '2106-10-17T11:36:00Z', 'RET', null, 'OOF20190627059903', '2106-10-30T11:32:00Z', - null, null, null, null, null, null, '', + null, null, null, -1, null, null, null, '', '{"returnStatus": "000"}', '2100-01-18T07:19:28.384921Z', 'ERS', 'JP/VISIOCaptures V1.4.7'); -- Add FLUX test data INSERT INTO logbook_reports (operation_number, operation_country, operation_datetime_utc, operation_type, report_id, - referenced_report_id, report_datetime_utc, cfr, ircs, external_identification, vessel_name, - flag_state, imo, log_type, value, integration_datetime_utc, trip_number, + referenced_report_id, report_datetime_utc, cfr, ircs, external_identification, vessel_id, + vessel_name, flag_state, imo, log_type, value, integration_datetime_utc, trip_number, transmission_format) VALUES ('cc7ee632-e515-460f-a1c1-f82222a6d419', null, '2020-05-06 18:40:51', 'DAT', - 'f006a2e5-0fdd-48a0-9a9a-ccae00d052d8', null, '2020-05-06 15:40:51', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', + 'f006a2e5-0fdd-48a0-9a9a-ccae00d052d8', null, '2020-05-06 15:40:51', 'SOCR4T3', 'IRCS6', 'XR006', 5, 'GOLF', 'CYP', '1234567', 'NOT_COX', '{"faoZoneExited": null, "latitudeExited": 57.7258, "longitudeExited": 0.5983, "effortZoneExited": null, "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:51.795Z", "statisticalRectangleExited": null}', '2022-03-31 09:21:19.378408', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('a3c52754-97e1-4a21-ba2e-d8f16f4544e9', null, '2020-05-06 18:40:57', 'DAT', - '9d1ddd34-1394-470e-b8a6-469b86150e1e', null, '2020-05-06 15:40:57', 'SOCR4T3', null, null, null, 'CYP', null, + '9d1ddd34-1394-470e-b8a6-469b86150e1e', null, '2020-05-06 15:40:57', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'COX', '{"faoZoneExited": null, "latitudeExited": 46.678, "longitudeExited": -14.616, "effortZoneExited": "A", "economicZoneExited": null, "targetSpeciesOnExit": null, "effortZoneExitDatetimeUtc": "2020-05-06T11:40:57.580Z", "statisticalRectangleExited": null}', '2022-03-31 09:21:19.384086', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('d5c3b039-aaee-4cca-bcae-637fa8effe14', null, '2020-05-06 18:41:03', 'DAT', - '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, null, 'CYP', null, + '7ee30c6c-adf9-4f60-a4f1-f7f15ab92803', null, '2020-05-06 15:41:03', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'PNO', '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z"}', '2022-03-31 09:21:19.38991', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('7cfcdde3-286c-4713-8460-2ed82a59be34', null, '2020-05-06 18:41:09', 'DAT', - 'fc16ea8a-3148-44b2-977f-de2a2ae550b9', null, '2020-05-06 15:41:09', 'SOCR4T3', null, null, null, 'CYP', null, + 'fc16ea8a-3148-44b2-977f-de2a2ae550b9', null, '2020-05-06 15:41:09', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'PNO', '{"port": "GBPHD", "purpose": "SHE", "tripStartDate": "2020-05-04T19:41:09.200Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:09.200Z"}', '2022-03-31 09:21:19.395805', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('4f971076-e6c6-48f6-b87e-deae90fe4705', null, '2020-05-06 18:41:15', 'DAT', - 'cc45063f-2d3c-4cda-ac0c-8381e279e150', null, '2020-05-06 15:41:15', 'SOCR4T3', null, null, 'GOLF', 'CYP', null, + 'cc45063f-2d3c-4cda-ac0c-8381e279e150', null, '2020-05-06 15:41:15', 'SOCR4T3', null, null, 5, 'GOLF', 'CYP', null, 'RTP', '{"port": "ESCAR", "reasonOfReturn": "REF", "returnDatetimeUtc": "2020-05-06T11:41:15.013Z"}', '2022-03-31 09:21:19.401686', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('8f06061e-e723-4b89-8577-3801a61582a2', null, '2020-05-06 18:41:20', 'DAT', - 'dde5df56-24c2-4a2e-8afb-561f32113256', null, '2020-05-06 15:41:20', 'SOCR4T3', 'IRCS6', 'XR006', null, 'CYP', + 'dde5df56-24c2-4a2e-8afb-561f32113256', null, '2020-05-06 15:41:20', 'SOCR4T3', 'IRCS6', 'XR006', 5, null, 'CYP', null, 'RTP', '{"port": "ESCAR", "gearOnboard": [{"gear": "GN", "mesh": 140.0, "dimensions": 1000.0}], "reasonOfReturn": "LAN", "returnDatetimeUtc": "2020-05-06T11:41:20.712Z"}', '2022-03-31 09:21:19.407777', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('8db132d1-68fc-4ae6-b12e-4af594351701', null, '2020-05-06 18:41:26', 'DAT', - '83952732-ef89-4168-b2a1-df49d0aa1aff', null, '2020-05-06 15:41:26', 'SOCR4T3', null, null, null, 'CYP', null, + '83952732-ef89-4168-b2a1-df49d0aa1aff', null, '2020-05-06 15:41:26', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'LAN', '{"port": "ESCAR", "sender": null, "catchLanded": [{"nbFish": null, "weight": 100.0, "faoZone": "27.9.b.2", "species": "HAD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.2, "preservationState": "FRO", "statisticalRectangle": null}], "landingDatetimeUtc": "2020-05-05T19:41:26.516Z"}', '2022-03-31 09:21:19.414081', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('b509d82f-ce27-46c2-b5a3-d2bcae09de8a', null, '2020-05-06 18:41:32', 'DAT', - 'ddf8f969-86f1-4eb9-a9a6-d61067a846bf', null, '2020-05-06 15:41:32', 'SOCR4T3', null, null, null, 'SVN', null, + 'ddf8f969-86f1-4eb9-a9a6-d61067a846bf', null, '2020-05-06 15:41:32', 'SOCR4T3', null, null, 5, null, 'SVN', null, 'TRA', 'null', '2022-03-31 09:21:19.420333', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('6c26236d-51ad-4aee-ac37-8e83978346a0', null, '2020-05-06 18:41:38', 'DAT', - 'b581876a-ae95-4a07-8b56-b6b5d8098a57', null, '2020-05-06 15:41:38', 'SOCR4T3', null, null, null, 'SVN', null, + 'b581876a-ae95-4a07-8b56-b6b5d8098a57', null, '2020-05-06 15:41:38', 'SOCR4T3', null, null, 5, null, 'SVN', null, 'TRA', 'null', '2022-03-31 09:21:19.426686', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('81cf0182-db9c-4384-aca3-a75b1067c41a', null, '2020-05-06 18:41:43', 'DAT', - 'ce5c46ca-3912-4de1-931c-d66b801b5362', null, '2020-05-06 15:41:43', 'SOCR4T3', null, null, null, 'CYP', null, + 'ce5c46ca-3912-4de1-931c-d66b801b5362', null, '2020-05-06 15:41:43', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'NOT_TRA', 'null', '2022-03-31 09:21:19.433052', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('ab1058c7-b7cf-4345-a0b3-a9f472cc6ef6', null, '2020-05-06 18:41:49', 'DAT', - 'e43c3bf0-163c-4fb0-a1de-1a61beb87988', null, '2020-05-06 15:41:49', 'SOCR4T3', 'IRCS6', 'XR006', null, 'CYP', + 'e43c3bf0-163c-4fb0-a1de-1a61beb87988', null, '2020-05-06 15:41:49', 'SOCR4T3', 'IRCS6', 'XR006', 5, null, 'CYP', '1234567', 'NOT_TRA', 'null', '2022-03-31 09:21:19.439501', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('8826952f-b240-4570-a9dc-59f3a24c7bf1', null, '2020-05-06 18:39:33', 'DAT', - '1e1bff95-dfff-4cc3-82d3-d72b46fda745', null, '2020-05-06 15:39:33', 'SOCR4T3', null, null, 'GOLF', 'CYP', + '1e1bff95-dfff-4cc3-82d3-d72b46fda745', null, '2020-05-06 15:39:33', 'SOCR4T3', null, null, 5, 'GOLF', 'CYP', '1234567', 'DEP', '{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "speciesOnboard": [{"nbFish": null, "weight": 50.0, "faoZone": "27.9.b.2", "species": "COD", "freshness": null, "packaging": "BOX", "effortZone": null, "economicZone": "ESP", "presentation": "GUT", "conversionFactor": 1.1, "preservationState": "FRO", "statisticalRectangle": null}], "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:33.176Z"}', '2022-03-31 09:21:19.501868', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('5ee8be46-2efe-4a29-b2df-bdf2d3ed66a1', null, '2020-05-06 18:39:40', 'DAT', - '7712fe73-cef2-4646-97bb-d634fde00b07', null, '2020-05-06 15:39:40', 'SOCR4T3', null, null, 'GOLF', 'CYP', + '7712fe73-cef2-4646-97bb-d634fde00b07', null, '2020-05-06 15:39:40', 'SOCR4T3', null, null, 5, 'GOLF', 'CYP', '1234567', 'DEP', '{"gearOnboard": [{"gear": "PS", "mesh": 140.0, "dimensions": 14.0}], "departurePort": "ESCAR", "anticipatedActivity": "FIS", "departureDatetimeUtc": "2020-05-06T11:39:40.722Z"}', '2022-03-31 09:21:19.507524', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('48794a8f-adfa-43b2-b4c3-2e8d3581bfb4', null, '2020-05-06 18:39:46', 'DAT', - '2843bd5b-e4e7-4816-8372-76805201301e', null, '2020-05-06 15:39:46', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', + '2843bd5b-e4e7-4816-8372-76805201301e', null, '2020-05-06 15:39:46', 'SOCR4T3', 'IRCS6', 'XR006', 5, 'GOLF', 'CYP', '1234567', 'NOT_COE', '{"latitudeEntered": 42.794, "longitudeEntered": -13.809, "faoZoneEntered": null, "effortZoneEntered": null, "economicZoneEntered": null, "targetSpeciesOnEntry": null, "effortZoneEntryDatetimeUtc": "2020-05-06T11:39:46.583Z", "statisticalRectangleEntered": null}', '2022-03-31 09:21:19.513305', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('196aca16-da66-4077-b340-ecad701be662', null, '2020-05-06 18:39:59', 'DAT', - 'b2fca5fb-d1cd-4ec7-8a8c-645cecab6866', null, '2020-05-06 15:39:59', 'SOCR4T3', null, null, null, 'CYP', null, + 'b2fca5fb-d1cd-4ec7-8a8c-645cecab6866', null, '2020-05-06 15:39:59', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'FAR', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-06T11:39:59.462Z"}]}', '2022-03-31 09:21:19.519424', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('4a4c8d24-f4be-4ccb-8aef-99ab5aae7e02', null, '2020-05-06 18:40:05', 'DAT', - '1a87f3de-dea9-4018-8c2e-d6cdfa97318e', null, '2020-05-06 15:40:05', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', + '1a87f3de-dea9-4018-8c2e-d6cdfa97318e', null, '2020-05-06 15:40:05', 'SOCR4T3', 'IRCS6', 'XR006', 5, 'GOLF', 'CYP', '1234567', 'FAR', '{"hauls": [{"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 1000.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D5"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}, {"gear": "TBB", "mesh": 140.0, "catches": [{"nbFish": null, "weight": 600.0, "faoZone": "27.8.e.1", "species": "COD", "effortZone": null, "economicZone": null, "statisticalRectangle": "21D6"}], "dimensions": 250.0, "farDatetimeUtc": "2020-05-04T19:40:05.354Z"}]}', '2022-03-31 09:21:19.525832', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('251db84c-1d8b-49be-b426-f70bb2c68a2d', null, '2020-05-06 18:40:11', 'DAT', - 'fe7acdb9-ff2e-4cfa-91a9-fd2e06b556e1', null, '2020-05-06 15:40:11', 'SOCR4T3', null, null, null, 'CYP', null, + 'fe7acdb9-ff2e-4cfa-91a9-fd2e06b556e1', null, '2020-05-06 15:40:11', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'FAR', '{"hauls": [{"farDatetimeUtc": "2020-05-06T11:40:11.291Z"}]}', '2022-03-31 09:21:19.531881', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('08a125d6-6b6d-4f90-b26a-bf8426673eea', null, '2020-05-06 18:40:17', 'DAT', - '74fcd0f7-8117-4791-9aa3-37d5c7dce880', null, '2020-05-06 15:40:17', 'SOCR4T3', null, null, null, 'SVN', null, + '74fcd0f7-8117-4791-9aa3-37d5c7dce880', null, '2020-05-06 15:40:17', 'SOCR4T3', null, null, 5, null, 'SVN', null, 'FAR', '{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "species": "BFT"}], "latitude": 39.65, "longitude": 6.83, "farDatetimeUtc": "2020-04-29T12:00:00.000Z"}]}', '2022-03-31 09:21:19.538061', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('9e38840b-f05a-49a4-ab34-e41131749fd0', null, '2020-05-06 18:40:22', 'DAT', - '1706938b-c3c8-4d34-b32f-54c8d2c0705a', null, '2020-05-06 15:40:22', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', + '1706938b-c3c8-4d34-b32f-54c8d2c0705a', null, '2020-05-06 15:40:22', 'SOCR4T3', 'IRCS6', 'XR006', 5, 'GOLF', 'CYP', '1234567', 'FAR', '{"hauls": [{"catches": [{"nbFish": null, "weight": 0.0, "faoZone": "27.8.e.1", "species": "MZZ", "effortZone": null, "economicZone": null, "statisticalRectangle": null}], "farDatetimeUtc": "2020-05-06T11:40:22.885Z"}]}', '2022-03-31 09:21:19.544336', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('60e0d2e0-2713-43d7-9fa1-fcf968e34d82', null, '2020-05-06 18:40:28', 'DAT', - 'a36d23c5-b339-455d-9b0b-bf766a9d57d9', null, '2020-05-06 15:40:28', 'SOCR4T3', 'IRCS6', 'XR006', 'GOLF', 'CYP', + 'a36d23c5-b339-455d-9b0b-bf766a9d57d9', null, '2020-05-06 15:40:28', 'SOCR4T3', 'IRCS6', 'XR006', 5, 'GOLF', 'CYP', '1234567', 'JFO', 'null', '2022-03-31 09:21:19.550891', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('0e1ea2b6-f4f5-4958-bc48-cfb016a22f58', null, '2020-05-06 18:40:34', 'DAT', - 'a913a52e-5e66-4f40-8c64-148f90fa8cd9', null, '2020-05-06 15:40:34', 'SOCR4T3', null, null, null, 'CYP', null, + 'a913a52e-5e66-4f40-8c64-148f90fa8cd9', null, '2020-05-06 15:40:34', 'SOCR4T3', null, null, 5, null, 'CYP', null, 'DIS', '{"catches": [{"nbFish": null, "weight": 100.0, "species": "COD"}], "discardDatetimeUtc": "2020-05-06T11:40:34.449Z"}', '2022-03-31 09:21:19.557299', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('3cffa378-0f8c-4540-b849-747621cfcb4a', null, '2020-05-06 18:40:40', 'DAT', - '7b487ada-019c-4b62-be32-7d15f7718344', null, '2020-05-06 15:40:40', 'SOCR4T3', null, null, null, 'CYP', + '7b487ada-019c-4b62-be32-7d15f7718344', null, '2020-05-06 15:40:40', 'SOCR4T3', null, null, 5, null, 'CYP', '1234567', 'RLC', 'null', '2022-03-31 09:21:19.563768', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('7bf7401d-cbb1-4e6f-bad8-7e309ee004cf', null, '2020-05-06 18:40:45', 'DAT', - 'ced42f65-a1ac-40e1-93c7-851d4933f770', null, '2020-05-06 15:40:45', 'SOCR4T3', null, null, 'GOLF', 'CYP', null, + 'ced42f65-a1ac-40e1-93c7-851d4933f770', null, '2020-05-06 15:40:45', 'SOCR4T3', null, null, 5, 'GOLF', 'CYP', null, 'RLC', 'null', '2022-03-31 09:21:19.570417', 'SRC-TRP-TTT20200506194051795', 'FLUX'), ('9376ccbd-be2f-4d3d-b4ac-3c559ac9586a', null, '2021-01-31 12:29:02', 'DAT', - '8eec0190-c353-4147-8a65-fcc697fbadbc', null, '2021-01-22 09:02:47', 'SOCR4T3', 'OPUF', 'Z.510', 'Dennis', + '8eec0190-c353-4147-8a65-fcc697fbadbc', null, '2021-01-22 09:02:47', 'SOCR4T3', 'OPUF', 'Z.510', 5, 'Dennis', 'BEL', null, 'COE', '{"latitudeEntered": 51.333333, "longitudeEntered": 3.2, "faoZoneEntered": "27.4.c", "effortZoneEntered": null, "economicZoneEntered": "BEL", "targetSpeciesOnEntry": "DEMERSAL", "effortZoneEntryDatetimeUtc": "2021-01-22T09:00:00Z", "statisticalRectangleEntered": "31F3"}', '2022-03-31 09:21:19.496049', 'SRC-TRP-TTT20200506194051795', 'FLUX'); @@ -651,25 +651,25 @@ INSERT INTO logbook_reports (operation_number, trip_number, operation_country, operation_datetime_utc, operation_type, report_id, referenced_report_id, report_datetime_utc, - cfr, ircs, external_identification, vessel_name, flag_state, imo, log_type, value, - integration_datetime_utc, transmission_format, software) + cfr, ircs, external_identification, vessel_id, vessel_name, flag_state, imo, log_type, + value, integration_datetime_utc, transmission_format, software) VALUES ('OOF20190439686456', 20230086, 'OOF', CURRENT_DATE - INTERVAL '5 days', 'DAT', 'OOF20190439686456', null, CURRENT_DATE - INTERVAL '5 days', - 'FR263454484', 'FE4864', '8FR6541', 'NO NAME', 'FRA', null, 'LAN', + 'FR263454484', 'FE4864', '8FR6541', 4, 'NO NAME', 'FRA', null, 'LAN', '{"port": "AEJAZ", "catchLanded": [{"weight": 40.0, "nbFish": null, "species": "SCR", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}, {"weight": 2.0, "nbFish": null, "species": "LBE", "faoZone": "27.8.a", "freshness": null, "packaging": "CNT", "effortZone": "C", "presentation": "WHL", "economicZone": "FRA", "preservationState": "ALI", "statisticalRectangle": "23E6"}], "landingDatetimeUtc": "2018-09-03T12:18Z"}', CURRENT_DATE - INTERVAL '5 days', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190158541231', 20230087, 'OOF', CURRENT_DATE - INTERVAL '4 days', 'DAT', 'OOF20190158541231', null, CURRENT_DATE - INTERVAL '4 days', - 'FR263454484', 'FE4864', '8FR6541', 'NO NAME', 'FRA', null, 'DEP', + 'FR263454484', 'FE4864', '8FR6541', 4, 'NO NAME', 'FRA', null, 'DEP', '{"gearOnboard": [{"gear": "GTR", "mesh": 100.0}], "departurePort": "AEJAZ", "anticipatedActivity": "FSH", "tripStartDate": "2018-02-17T00:00Z", "departureDatetimeUtc": "2018-02-17T01:05Z"}', CURRENT_DATE - INTERVAL '4 days', 'ERS', 'TurboCatch (3.7-1)'), ('OOF20190439686457', 20230087, 'OOF', CURRENT_DATE - INTERVAL '3 days', 'DAT', 'OOF20190439686457', null, CURRENT_DATE - INTERVAL '3 days', - 'FR263454484', 'FE4864', '8FR6541', 'NO NAME', 'FRA', null, 'PNO', + 'FR263454484', 'FE4864', '8FR6541', 4, 'NO NAME', 'FRA', null, 'PNO', '{"port": "AEJAZ", "purpose": "LAN", "catchOnboard": [{"weight": 25.0, "nbFish": null, "species": "SOL", "faoZone": "27.8.a", "effortZone": "C", "economicZone": "FRA", "statisticalRectangle": "23E6"}], "tripStartDate": "2018-02-20T00:00Z", "predictedArrivalDatetimeUtc": "2018-02-20T13:38Z"}', CURRENT_DATE - INTERVAL '3 days', 'ERS', 'TurboCatch (3.7-1)'), ('d5c3b039-aaee-4cca-bcae-637f5fe574f5', 'SRC-TRP-TTT20200506194051795', null, CURRENT_DATE - INTERVAL '2 days', 'DAT', - 'd5c3b039-aaee-4cca-bcae-637f5fe574f5', null, CURRENT_DATE - INTERVAL '2 days', 'FR263454484', 'FE4864', '8FR6541', 'NO NAME', 'FRA', null, + 'd5c3b039-aaee-4cca-bcae-637f5fe574f5', null, CURRENT_DATE - INTERVAL '2 days', 'FR263454484', 'FE4864', '8FR6541', 4, 'NO NAME', 'FRA', null, 'PNO', '{"port": "GBPHD", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "weight": 1500.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z"}', CURRENT_DATE - INTERVAL '2 days', 'FLUX', null); @@ -744,12 +744,12 @@ SET { "pnoTypeName": "Préavis type X", "minimumNotificationPeriod": 4.0, - "hasDesignated_ports": false + "hasDesignatedPorts": false }, { "pnoTypeName": "Préavis type Y", "minimumNotificationPeriod": 8.0, - "hasDesignated_ports": true + "hasDesignatedPorts": true } ]'::jsonb ) diff --git a/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc index 3df5d13c0e..2f903429a9 100644 --- a/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc +++ b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.jsonc @@ -1,32 +1,39 @@ [ { "table": "logbook_raw_messages", - "data": [{ "operation_number": "FAKE_OPERATION_101" }, { "operation_number": "FAKE_OPERATION_102" }] + "data": [ + { "operation_number": "FAKE_OPERATION_101" }, + { "operation_number": "FAKE_OPERATION_102" }, + { "operation_number": "FAKE_OPERATION_103" }, + { "operation_number": "FAKE_OPERATION_104" }, + { "operation_number": "FAKE_OPERATION_105" }, + { "operation_number": "FAKE_OPERATION_106" }, + { "operation_number": "FAKE_OPERATION_107" }, + { "operation_number": "FAKE_OPERATION_108" }, + { "operation_number": "FAKE_OPERATION_109" }, + { "operation_number": "FAKE_OPERATION_110" }, + { "operation_number": "FAKE_OPERATION_111" }, + { "operation_number": "FAKE_OPERATION_112" } + ] }, { "table": "logbook_reports", "data": [ + // - Vessel: PHENOMENE + // - Purpose: LAN // - Without reporting { "id": 101, "cfr": "FAK000999999", "enriched": true, - "external_identification": "DONTSINK", - "flag_state": "FRA", - "imo": null, "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", - "ircs": "CALLME", "log_type": "PNO", - "operation_country": "OOF", "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", "operation_number": "FAKE_OPERATION_101", "operation_type": "DAT", - "referenced_report_id": null, "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", - "report_id": "FAKE_REPORT_101", - "software": null, "transmission_format": "ERS", - "trip_number": 10000001, + "vessel_id": 1, "vessel_name": "PHENOMENE", "trip_gears:jsonb": [ { "gear": "GTR", "mesh": 100, "dimensions": "250;180" }, @@ -50,45 +57,39 @@ ], "pnoTypes": [ { - "pnoTypeName": "Préavis type X", + "pnoTypeName": "Préavis type A", "minimumNotificationPeriod": 4.0, - "hasDesignated_ports": false + "hasDesignatedPorts": false }, { - "pnoTypeName": "Préavis type Y", + "pnoTypeName": "Préavis type B", "minimumNotificationPeriod": 8.0, - "hasDesignated_ports": true + "hasDesignatedPorts": true } ], "port": "FRSML", "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", - "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", "purpose": "LAN", "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" } }, - // - With reporting + // - Vessel: COURANT MAIN PROFESSEUR + // - Purpose: LAN + // - With reportings { "id": 102, "cfr": "ABC000042310", "enriched": true, - "external_identification": "IW783219", - "flag_state": "FRA", - "imo": null, "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", - "ircs": "QD0506", "log_type": "PNO", - "operation_country": "OOF", "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", "operation_number": "FAKE_OPERATION_102", "operation_type": "DAT", - "referenced_report_id": null, "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", - "report_id": "FAKE_REPORT_102", - "software": null, "transmission_format": "ERS", - "trip_number": 10000002, + "vessel_id": 10, "vessel_name": "COURANT MAIN PROFESSEUR", "trip_gears:jsonb": [ { "gear": "GTR", "mesh": 100, "dimensions": "250;180" }, @@ -112,22 +113,43 @@ ], "pnoTypes": [ { - "pnoTypeName": "Préavis type X", + "pnoTypeName": "Préavis type C", "minimumNotificationPeriod": 4.0, - "hasDesignated_ports": false - }, - { - "pnoTypeName": "Préavis type Y", - "minimumNotificationPeriod": 8.0, - "hasDesignated_ports": true + "hasDesignatedPorts": false } ], - "port": "FRSML", + "port": "AEFRP", "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", "purpose": "LAN", "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" } + }, + + // - Vessel: UNKNOWN + // - Purpose: GRD + // - Without reporting + { + "id": 103, + "enriched": true, + "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "log_type": "PNO", + "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "operation_number": "FAKE_OPERATION_103", + "operation_type": "DAT", + "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC'", + "transmission_format": "ERS", + "vessel_id": -1, + "trip_gears:jsonb": [], + "trip_segments:jsonb": [], + "value:jsonb": { + "catchOnboard": [], + "pnoTypes": [], + "port": "AEHZP", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "GRD", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } } ] } diff --git a/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql index 186f637bad..b51687f50c 100644 --- a/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql +++ b/backend/src/main/resources/db/testdata/V666.5.1__Insert_more_pno_logbook_reports.sql @@ -2,12 +2,36 @@ INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_101' INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_102'); -INSERT INTO logbook_reports (id, cfr, enriched, external_identification, flag_state, imo, integration_datetime_utc, ircs, log_type, operation_country, operation_datetime_utc, operation_number, operation_type, referenced_report_id, report_datetime_utc, report_id, software, transmission_format, trip_number, vessel_name, trip_gears, trip_segments, value) VALUES (101, 'FAK000999999', true, 'DONTSINK', 'FRA', null, NOW() AT TIME ZONE 'UTC', 'CALLME', 'PNO', 'OOF', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_101', 'DAT', null, NOW() AT TIME ZONE 'UTC', 'FAKE_REPORT_101', null, 'ERS', 10000001, 'PHENOMENE', '[{"gear":"GTR","mesh":100,"dimensions":"250;180"},{"gear":"GTR","mesh":120.5,"dimensions":"250;280"}]', '[{"segment":"NWW01","segment_name":"Chalutiers de fond"},{"segment":"PEL01","segment_name":"Chalutiers pélagiques"}]', '{"catchOnboard":[{"weight":25,"nbFish":null,"species":"SOL","faoZone":"27.8.a","effortZone":"C","economicZone":"FRA","statisticalRectangle":"23E6"}],"pnoTypes":[{"pnoTypeName":"Préavis type X","minimumNotificationPeriod":4,"hasDesignated_ports":false},{"pnoTypeName":"Préavis type Y","minimumNotificationPeriod":8,"hasDesignated_ports":true}],"port":"FRSML","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_103'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_104'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_105'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_106'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_107'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_108'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_109'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_110'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_111'); + +INSERT INTO logbook_raw_messages (operation_number) VALUES ('FAKE_OPERATION_112'); + +INSERT INTO logbook_reports (id, cfr, enriched, integration_datetime_utc, log_type, operation_datetime_utc, operation_number, operation_type, report_datetime_utc, transmission_format, vessel_id, vessel_name, trip_gears, trip_segments, value) VALUES (101, 'FAK000999999', true, NOW() AT TIME ZONE 'UTC', 'PNO', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_101', 'DAT', NOW() AT TIME ZONE 'UTC', 'ERS', 1, 'PHENOMENE', '[{"gear":"GTR","mesh":100,"dimensions":"250;180"},{"gear":"GTR","mesh":120.5,"dimensions":"250;280"}]', '[{"segment":"NWW01","segment_name":"Chalutiers de fond"},{"segment":"PEL01","segment_name":"Chalutiers pélagiques"}]', '{"catchOnboard":[{"weight":25,"nbFish":null,"species":"SOL","faoZone":"27.8.a","effortZone":"C","economicZone":"FRA","statisticalRectangle":"23E6"}],"pnoTypes":[{"pnoTypeName":"Préavis type A","minimumNotificationPeriod":4,"hasDesignatedPorts":false},{"pnoTypeName":"Préavis type B","minimumNotificationPeriod":8,"hasDesignatedPorts":true}],"port":"FRSML","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; -UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; +UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; UPDATE logbook_reports SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 101; -INSERT INTO logbook_reports (id, cfr, enriched, external_identification, flag_state, imo, integration_datetime_utc, ircs, log_type, operation_country, operation_datetime_utc, operation_number, operation_type, referenced_report_id, report_datetime_utc, report_id, software, transmission_format, trip_number, vessel_name, trip_gears, trip_segments, value) VALUES (102, 'ABC000042310', true, 'IW783219', 'FRA', null, NOW() AT TIME ZONE 'UTC', 'QD0506', 'PNO', 'OOF', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_102', 'DAT', null, NOW() AT TIME ZONE 'UTC', 'FAKE_REPORT_102', null, 'ERS', 10000002, 'COURANT MAIN PROFESSEUR', '[{"gear":"GTR","mesh":100,"dimensions":"250;180"},{"gear":"GTR","mesh":120.5,"dimensions":"250;280"}]', '[{"segment":"NWW01","segment_name":"Chalutiers de fond"},{"segment":"PEL01","segment_name":"Chalutiers pélagiques"}]', '{"catchOnboard":[{"weight":25,"nbFish":null,"species":"SOL","faoZone":"27.8.a","effortZone":"C","economicZone":"FRA","statisticalRectangle":"23E6"}],"pnoTypes":[{"pnoTypeName":"Préavis type X","minimumNotificationPeriod":4,"hasDesignated_ports":false},{"pnoTypeName":"Préavis type Y","minimumNotificationPeriod":8,"hasDesignated_ports":true}],"port":"FRSML","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +INSERT INTO logbook_reports (id, cfr, enriched, integration_datetime_utc, log_type, operation_datetime_utc, operation_number, operation_type, report_datetime_utc, transmission_format, vessel_id, vessel_name, trip_gears, trip_segments, value) VALUES (102, 'ABC000042310', true, NOW() AT TIME ZONE 'UTC', 'PNO', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_102', 'DAT', NOW() AT TIME ZONE 'UTC', 'ERS', 10, 'COURANT MAIN PROFESSEUR', '[{"gear":"GTR","mesh":100,"dimensions":"250;180"},{"gear":"GTR","mesh":120.5,"dimensions":"250;280"}]', '[{"segment":"NWW01","segment_name":"Chalutiers de fond"},{"segment":"PEL01","segment_name":"Chalutiers pélagiques"}]', '{"catchOnboard":[{"weight":25,"nbFish":null,"species":"SOL","faoZone":"27.8.a","effortZone":"C","economicZone":"FRA","statisticalRectangle":"23E6"}],"pnoTypes":[{"pnoTypeName":"Préavis type C","minimumNotificationPeriod":4,"hasDesignatedPorts":false}],"port":"AEFRP","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 102; UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 102; UPDATE logbook_reports SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 102; + +INSERT INTO logbook_reports (id, enriched, integration_datetime_utc, log_type, operation_datetime_utc, operation_number, operation_type, report_datetime_utc, transmission_format, vessel_id, trip_gears, trip_segments, value) VALUES (103, true, NOW() AT TIME ZONE 'UTC', 'PNO', NOW() AT TIME ZONE 'UTC', 'FAKE_OPERATION_103', 'DAT', NOW() AT TIME ZONE 'UTC', 'ERS', -1, '[]', '[]', '{"catchOnboard":[],"pnoTypes":[],"port":"AEHZP","predictedArrivalDatetimeUtc":null,"purpose":"GRD","tripStartDate":null}'); +UPDATE logbook_reports SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 103; +UPDATE logbook_reports SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE id = 103; diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFacadeAreasRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFacadeAreasRepositoryITests.kt index c2d2d62004..d2fde968cc 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFacadeAreasRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFacadeAreasRepositoryITests.kt @@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.transaction.annotation.Transactional class JpaFacadeAreasRepositoryITests : AbstractDBTests() { - @Autowired private lateinit var facadeAreasRepository: FacadeAreasRepository diff --git a/frontend/src/components/Ellipsised.tsx b/frontend/src/components/Ellipsised.tsx new file mode 100644 index 0000000000..4b17598e8e --- /dev/null +++ b/frontend/src/components/Ellipsised.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components' + +export const Ellipsised = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` diff --git a/frontend/src/domain/types/port.ts b/frontend/src/domain/types/port.ts index 0bcde4d911..d0a15eba53 100644 --- a/frontend/src/domain/types/port.ts +++ b/frontend/src/domain/types/port.ts @@ -5,5 +5,6 @@ export namespace Port { locode: string longitude: number | undefined name: string + region: string | undefined } } diff --git a/frontend/src/features/Logbook/Logbook.types.ts b/frontend/src/features/Logbook/Logbook.types.ts index 71137795c9..e0834c76c4 100644 --- a/frontend/src/features/Logbook/Logbook.types.ts +++ b/frontend/src/features/Logbook/Logbook.types.ts @@ -15,7 +15,7 @@ export type FishingActivities = { logbookMessages: LogbookMessage[] } -// TODO Replace this type with `LogbookMessage.LogbookMessage`. +// TODO Replace with `LogbookMessage.LogbookMessage`. export type LogbookMessage = { acknowledge: { dateTime: string | null @@ -65,6 +65,7 @@ export type PNOAndLANWeightToleranceAlertValue = { type: 'PNO_LAN_WEIGHT_TOLERANCE_ALERT' } +// TODO Replace this type with `LogbookMessage.Catch`. export type LogbookCatch = { conversionFactor: number | null economicZone: string | null @@ -81,6 +82,7 @@ export type LogbookCatch = { weight: number | null } +// TODO Duplicate with `LogbookMessage.TripGear`? export type Gear = { dimensions: string gear: string diff --git a/frontend/src/features/Logbook/LogbookMessage.types.ts b/frontend/src/features/Logbook/LogbookMessage.types.ts index 0d8cfcf5af..dd5c5ef562 100644 --- a/frontend/src/features/Logbook/LogbookMessage.types.ts +++ b/frontend/src/features/Logbook/LogbookMessage.types.ts @@ -1,5 +1,5 @@ import type { Vessel } from '@features/Vessel/Vessel.types' -import type { Undefine } from '@mtes-mct/monitor-ui' +import type { Undefine, UndefineExcept } from '@mtes-mct/monitor-ui' export namespace LogbookMessage { export type LogbookMessage = { @@ -33,6 +33,25 @@ export namespace LogbookMessage { returnStatus: string | undefined } + export type Catch = UndefineExcept< + { + conversionFactor: number + economicZone: string + effortZone: string + faoZone: string + freshness: string + numberFish: number + packaging: string + presentation: string + preservationState: string + species: string + speciesName: string + statisticalRectangle: string + weight: number + }, + 'species' + > + export type Message = Undefine<{ catchOnboard: MessageCatchonboard[] economicZone: string @@ -68,7 +87,7 @@ export namespace LogbookMessage { } export type MessagePnoType = { - hasDesignated_ports: boolean + hasDesignatedPorts: boolean minimumNotificationPeriod: number // TODO Replace that with an enum. pnoTypeName: string @@ -82,19 +101,24 @@ export namespace LogbookMessage { } export type TripSegment = { - segment: string - segmentName: string + code: string + name: string } - export type ApiFilter = Undefine<{ - flagStates: string[] - integratedAfter: string - integratedBefore: string - portLocodes: string[] - searchQuery: string - specyCodes: string[] - tripGearCodes: string[] - tripSegmentSegments: string[] - vesselId: Vessel.VesselId - }> + export type ApiFilter = Partial< + Undefine<{ + flagStates: string[] + isLessThanTwelveMetersVessel: boolean + lastControlledAfter: string + lastControlledBefore: string + portLocodes: string[] + searchQuery: string + specyCodes: string[] + tripGearCodes: string[] + tripSegmentSegments: string[] + vesselLength: Vessel.VesselId + willArriveAfter: string + willArriveBefore: string + }> + > } diff --git a/frontend/src/features/Logbook/constants.ts b/frontend/src/features/Logbook/constants.ts index 5024f89fa3..a555d4388b 100644 --- a/frontend/src/features/Logbook/constants.ts +++ b/frontend/src/features/Logbook/constants.ts @@ -187,6 +187,7 @@ export const LogbookMessageSender = { MAS: 'Capitaine' } +// TODO Replace with `PriorNotification.PurposeCode`. export const LogbookMessagePNOPurposeType = { LAN: 'Débarquement', SHE: "Mise à l'abri" diff --git a/frontend/src/features/PriorNotification/PriorNotification.types.ts b/frontend/src/features/PriorNotification/PriorNotification.types.ts index 786eb16e4b..ee06c7a6e3 100644 --- a/frontend/src/features/PriorNotification/PriorNotification.types.ts +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -1,63 +1,83 @@ // import type { SeaFrontGroup } from '../../domain/entities/seaFront/constants' -import type { Vessel } from '../../domain/entities/vessel/types' -import type { Port } from '../../domain/types/port' import type { LogbookMessage } from '@features/Logbook/LogbookMessage.types' -import type { RiskFactor } from 'domain/entities/vessel/riskFactor/types' +import type { UndefineExcept } from '@mtes-mct/monitor-ui' +import type { SeaFront } from 'domain/entities/seaFront/constants' export namespace PriorNotification { - export interface PriorNotification { - id: number - logbookMessage: LogbookMessage.LogbookMessage | undefined - // TODO Real time or pre-calculated and stored? - port: Port.Port | undefined - // TODO Real time or pre-calculated and stored? - reportingsCount: number - // TODO Is it a seaFront or a seaFrontGroup? - // TODO Replace with enum. - seaFront: string | undefined - tripGears: LogbookMessage.TripGear[] - tripSegments: LogbookMessage.TripSegment[] - // TODO Real time or pre-calculated and stored? - vessel: Vessel | undefined - // TODO Real time or pre-calculated and stored? - vesselRiskFactor: RiskFactor | undefined - } + export type PriorNotification = UndefineExcept< + { + expectedArrivalDate: string + expectedLandingDate: string + id: number + isVesselUnderCharter: boolean + notificationTypeLabel: string + onBoardCatches: LogbookMessage.Catch[] + portLocode: string + portName: string + purposeCode: PurposeCode + reportingsCount: number + seaFront: SeaFront + sentAt: string + tripGears: LogbookMessage.TripGear[] + tripSegments: LogbookMessage.TripSegment[] + types: Type[] + vesselExternalReferenceNumber: string + vesselFlagCountryCode: string + // TODO Wait for vesselId in logbook reports (including "navire inconnu"). + vesselId: number + vesselInternalReferenceNumber: string + vesselIrcs: string + vesselLastControlDate: string + vesselLength: number + vesselMmsi: string + vesselName: string + vesselRiskFactor: number + vesselRiskFactorDetectability: number + vesselRiskFactorImpact: number + vesselRiskFactorProbability: number + }, + 'id' | 'onBoardCatches' | 'reportingsCount' | 'tripGears' | 'tripSegments' | 'types' + > - // TODO Fill all the possible case. Exiting labelled enum somewhere else? - export enum PriorNotificationReason { - LANDING = 'LANDING' - } - export const PRIOR_NOTIFICATION_REASON_LABEL: Record = { - LANDING: 'Débarquement' + export type Type = { + hasDesignatedPorts: number + minimumNotificationPeriod: string + name: string } - // TODO Check and update with datascience values. - /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/string-enum */ - export enum PriorNotificationType { - BASS = 'BASS', - DEEP_SEA_SPECIES = 'DEEP_SEA_SPECIES', - HAKE = 'HAKE', - SMALL_PELLAGIC_SPECIES = 'SMALL_PELLAGIC_SPECIES', - SOLE = 'SOLE', - BLUEFIN_THUMA = 'BLUEFIN_THUMA', - RED_CORAL = 'RED_CORAL', - SHORE_SEINE = 'SHORE_SEINE', - COMMUNITY = 'COMMUNITY', - THIRD_PARTY_VESSEL = 'THIRD_PARTY_VESSEL', - NOT_APPLICABLE = 'NOT_APPLICABLE' + // TODO Fill all the possible case. Exiting labelled enum somewhere else? + export enum PurposeCode { + ECY = 'ECY', + GRD = 'GRD', + LAN = 'LAN', + OTH = 'OTH', + REF = 'REF', + REP = 'REP', + RES = 'RES', + SCR = 'SCR', + SHE = 'SHE', + TRA = 'TRA' } - export const PRIOR_NOTIFICATION_TYPE_LABEL: Record = { - BASS: 'Bar', - DEEP_SEA_SPECIES: 'Espèces eaux profondes', - HAKE: 'Merlu', - SMALL_PELLAGIC_SPECIES: 'Petits pélagiques', - SOLE: 'Sole', - BLUEFIN_THUMA: 'Thon rouge', - RED_CORAL: 'Corail rouge', - SHORE_SEINE: 'Senne de plage', - COMMUNITY: 'Navire communautaire', - THIRD_PARTY_VESSEL: 'Navire tiers', - NOT_APPLICABLE: 'Non soumis' + export const PURPOSE_LABEL: Record = { + // "Emergency" + ECY: 'Urgence', + // "Vessels grounded and called by the authorities" + GRD: 'Immobilisation et convocation par les autorités', + // "Landing" + LAN: 'Débarquement', + // "Other" + OTH: 'Autre', + // "Refueling" + REF: 'Ravitaillement', + // "Repair" + REP: 'Réparation', + // "Rest" + RES: 'Repos', + // "Return for Scientific Research" + SCR: 'Retour pour Recherche Scientifique', + // "Sheltering" + SHE: 'Mise à l’abri', + // "Transhipment" + TRA: 'Transbordement' } - /* eslint-enable sort-keys-fix/sort-keys-fix */ } diff --git a/frontend/src/features/PriorNotification/api.ts b/frontend/src/features/PriorNotification/api.ts index a9fd1fca98..d5abecb2cb 100644 --- a/frontend/src/features/PriorNotification/api.ts +++ b/frontend/src/features/PriorNotification/api.ts @@ -10,8 +10,13 @@ export const priorNotificationApi = monitorfishApi.injectEndpoints({ getPriorNotifications: builder.query({ providesTags: () => [{ type: 'Notices' }], query: filter => getUrlOrPathWithQueryParams(`/prior-notifications`, filter) + }), + + getPriorNotificationTypes: builder.query({ + providesTags: () => [{ type: 'Notices' }], + query: () => '/prior-notifications/types' }) }) }) -export const { useGetPriorNotificationsQuery } = priorNotificationApi +export const { useGetPriorNotificationsQuery, useGetPriorNotificationTypesQuery } = priorNotificationApi diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx index 50edceb26a..a45edc4df8 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationList/FilterBar.tsx @@ -1,5 +1,5 @@ import { COUNTRIES_AS_ALPHA3_OPTIONS } from '@constants/index' -import { PriorNotification } from '@features/PriorNotification/PriorNotification.types' +import { useGetPriorNotificationTypesAsOptions } from '@features/PriorNotification/hooks/useGetPriorNotificationTypesAsOptions' import { useGetFleetSegmentsAsOptions } from '@hooks/useGetFleetSegmentsAsOptions' import { useGetGearsAsTreeOptions } from '@hooks/useGetGearsAsTreeOptions' import { useGetPortsAsTreeOptions } from '@hooks/useGetPortsAsTreeOptions' @@ -18,14 +18,15 @@ import { TextInput, type DateAsStringRange } from '@mtes-mct/monitor-ui' +import { assertNotNullish } from '@utils/assertNotNullish' +import { useCallback } from 'react' import styled from 'styled-components' import { LAST_CONTROL_PERIODS_AS_OPTIONS, LastControlPeriod, - PRIOR_NOTIFICATION_TYPES_AS_OPTIONS, - RECEIVED_AT_PERIODS_AS_OPTIONS, - ReceivedAtPeriod + EXPECTED_ARRIVAL_PERIODS_AS_OPTIONS, + ExpectedArrivalPeriod } from './constants' import { priorNotificationActions } from '../../slice' @@ -42,12 +43,28 @@ export function FilterBar() { const { gearsAsTreeOptions } = useGetGearsAsTreeOptions() const { portsAsTreeOptions } = useGetPortsAsTreeOptions() const { speciesAsOptions } = useGetSpeciesAsOptions() + const { typesAsOptions } = useGetPriorNotificationTypesAsOptions() const dispatch = useMainAppDispatch() const updateCountryCodes = (nextCountryCodes: string[] | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ countryCodes: nextCountryCodes })) } + const updateExpectedArrivalCustomPeriod = (nextExpectedArrivalCustomPeriod: DateAsStringRange | undefined) => { + dispatch( + priorNotificationActions.setListFilterValues({ expectedArrivalCustomPeriod: nextExpectedArrivalCustomPeriod }) + ) + } + + const updateExpectedArrivalPeriod = (nextexpectedArrivalPeriod: ExpectedArrivalPeriod | undefined) => { + assertNotNullish(nextexpectedArrivalPeriod) + + if (nextexpectedArrivalPeriod !== ExpectedArrivalPeriod.CUSTOM) { + dispatch(priorNotificationActions.setListFilterValues({ expectedArrivalCustomPeriod: undefined })) + } + dispatch(priorNotificationActions.setListFilterValues({ expectedArrivalPeriod: nextexpectedArrivalPeriod })) + } + const updateFleetSegments = (nextFleetSegmentSegments: string[] | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ fleetSegmentSegments: nextFleetSegmentSegments })) } @@ -56,9 +73,12 @@ export function FilterBar() { dispatch(priorNotificationActions.setListFilterValues({ gearCodes: nextGearCodes })) } - const updateHasOneOrMoreReportings = (nextHasOneOrMoreReportings: RichBoolean | undefined) => { - dispatch(priorNotificationActions.setListFilterValues({ hasOneOrMoreReportings: nextHasOneOrMoreReportings })) - } + const updateHasOneOrMoreReportings = useCallback( + (nextHasOneOrMoreReportings: RichBoolean | undefined) => { + dispatch(priorNotificationActions.setListFilterValues({ hasOneOrMoreReportings: nextHasOneOrMoreReportings })) + }, + [dispatch] + ) const updateIsLessThanTwelveMetersVessel = (nextIsLessThanTwelveMetersVessel: RichBoolean | undefined) => { dispatch( @@ -74,14 +94,6 @@ export function FilterBar() { dispatch(priorNotificationActions.setListFilterValues({ portLocodes: nextPortLocodes })) } - const updateReceivedAtCustomDateRange = (nextReceivedAtCustomDateRange: DateAsStringRange | undefined) => { - dispatch(priorNotificationActions.setListFilterValues({ receivedAtCustomDateRange: nextReceivedAtCustomDateRange })) - } - - const updateReceivedAtPeriod = (nextReceivedAtPeriod: ReceivedAtPeriod | undefined) => { - dispatch(priorNotificationActions.setListFilterValues({ receivedAtPeriod: nextReceivedAtPeriod })) - } - const updateSearchQuery = (nextSearchQuery: string | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ searchQuery: nextSearchQuery })) } @@ -90,7 +102,7 @@ export function FilterBar() { dispatch(priorNotificationActions.setListFilterValues({ specyCodes: nextSpecyCodes })) } - const updateTypes = (nextTypes: PriorNotification.PriorNotificationType[] | undefined) => { + const updateTypes = (nextTypes: string[] | undefined) => { dispatch(priorNotificationActions.setListFilterValues({ types: nextTypes })) } @@ -183,7 +195,7 @@ export function FilterBar() { isInline isLabelHidden label="Signalements" - name="isLessThanTwelveMetersVessel" + name="hasOneOrMoreReportings" onChange={updateHasOneOrMoreReportings} trueOptionLabel="Avec signalements" value={listFilterValues.hasOneOrMoreReportings} @@ -192,15 +204,16 @@ export function FilterBar() {

    + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + Voir plus de détail +