diff --git a/src/pages/observatoire.py b/src/pages/observatoire.py index 3b66447..525d5b3 100644 --- a/src/pages/observatoire.py +++ b/src/pages/observatoire.py @@ -18,6 +18,7 @@ from dash import ( ) from src.figures import ( + DataTable, get_barchart_sources, get_dashboard_summary_table, get_distance_histogram, @@ -25,14 +26,17 @@ from src.figures import ( get_geographic_maps, get_top_org_table, make_card, + make_column_picker, make_donut, ) from src.utils import ( + data_schema, departements, df, df_acheteurs, df_titulaires, get_enum_values_as_dict, + logger, meta_content, prepare_dashboard_data, ) @@ -61,6 +65,27 @@ for code in departements.keys(): } options_departements.append(departement) +OBSERVATOIRE_COLUMNS = [ + col + for col in df.columns + if col.startswith("acheteur") + or col.startswith("titulaire") + or col + in [ + "uid", + "dateNotification", + "montant", + "considerationsSociales", + "considerationsEnvironnementales", + "marcheInnovant", + "sousTraitanceDeclaree", + "techniques", + "sourceDataset", + "type", + "codeCPV", + ] +] + layout = [ dcc.Location(id="dashboard_url", refresh="callback-nav"), dcc.Store(id="observatoire-filters", storage_type="local"), @@ -372,10 +397,16 @@ Alors, on fait comment ? ), dcc.Download(id="download-observatoire"), dbc.Button( - "Télécharger au format Excel", - id="btn-download-observatoire", - disabled=True, + "Prévisualiser les données", + id="btn-observatoire-preview", className="mt-2", + color="primary", + outline=True, + ), + dcc.Input( + id="observatoire-share-url", + readOnly=True, + style={"display": "none"}, ), dcc.Input( id="observatoire-share-url", @@ -398,6 +429,83 @@ Alors, on fait comment ? ), ], ), + dbc.Offcanvas( + id="observatoire-preview", + title="Prévisualisation des données", + placement="bottom", + is_open=False, + scrollable=True, + style={"height": "75vh"}, + children=[ + # Header row: title + "Colonnes affichées" button + dbc.Row( + [ + dbc.Col( + html.Div( + className="table-menu", + children=[ + dbc.Button( + "Colonnes affichées", + id="observatoire-preview-columns-open", + size="sm", + color="secondary", + outline=True, + ), + dbc.Button( + "Télécharger au format Excel", + id="btn-download-observatoire", + disabled=True, + className="mt-2", + color="primary", + outline=True, + ), + ], + ), + width="auto", + ), + ], + className="mb-2 align-items-center", + ), + # Column picker modal + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Colonnes affichées dans la prévisualisation") + ), + dbc.ModalBody( + id="observatoire-preview-columns-body", + children=make_column_picker("observatoire_preview"), + ), + dbc.ModalFooter( + dbc.Button( + "Fermer", + id="observatoire-preview-columns-close", + className="ms-auto", + n_clicks=0, + ) + ), + ], + id="observatoire-preview-columns", + is_open=False, + fullscreen="md-down", + scrollable=True, + size="xl", + ), + # DataTable + html.Div( + className="marches_table", + children=DataTable( + dtid="observatoire-preview-table", + page_size=10, + page_action="native", + sort_action="native", + filter_action="native", + hidden_columns=[], + columns=[{"id": col, "name": col} for col in OBSERVATOIRE_COLUMNS], + ), + ), + ], + ), ] @@ -586,6 +694,9 @@ def udpate_dashboard_cards( # Génération des métriques dff = lff.collect(engine="streaming") + + logger.debug("Filter data: " + str(dff.height)) + df_per_uid = ( dff.select("uid", "montant").group_by("uid").agg(pl.col("montant").first()) ) @@ -795,3 +906,96 @@ def add_organization_name_in_title(acheteur_id, titulaire_id): html.Small(nom, className="text-muted d-block fw-normal fs-5"), ] return name + + +@callback( + Output("observatoire-preview", "is_open"), + Input("btn-observatoire-preview", "n_clicks"), + State("observatoire-preview", "is_open"), + prevent_initial_call=True, +) +def toggle_observatoire_preview(n_clicks, is_open): + return not is_open + + +@callback( + Output("observatoire-preview-table", "data"), + Output("observatoire-preview-table", "columns"), + Input("observatoire-preview", "is_open"), + State("dashboard_year", "value"), + State("dashboard_acheteur_id", "value"), + State("dashboard_acheteur_categorie", "value"), + State("dashboard_acheteur_departement_code", "value"), + State("dashboard_titulaire_id", "value"), + State("dashboard_titulaire_categorie", "value"), + State("dashboard_titulaire_departement_code", "value"), + State("dashboard_marche_type", "value"), + State("dashboard_marche_objet", "value"), + State("dashboard_marche_code_cpv", "value"), + State("dashboard_montant_min", "value"), + State("dashboard_montant_max", "value"), + State("dashboard_marche_techniques", "value"), + State("dashboard_marche_innovant", "value"), + State("dashboard_marche_sousTraitanceDeclaree", "value"), + State("dashboard_marche_considerationsSociales", "value"), + State("dashboard_marche_considerationsEnvironnementales", "value"), + prevent_initial_call=True, +) +def populate_preview_table( + is_open, + dashboard_year, + dashboard_acheteur_id, + dashboard_acheteur_categorie, + dashboard_acheteur_departement_code, + dashboard_titulaire_id, + dashboard_titulaire_categorie, + dashboard_titulaire_departement_code, + dashboard_marche_type, + dashboard_marche_objet, + dashboard_marche_code_cpv, + dashboard_montant_min, + dashboard_montant_max, + dashboard_marche_techniques, + dashboard_marche_innovant, + dashboard_marche_sous_traitance_declaree, + dashboard_marche_considerations_sociales, + dashboard_marche_considerations_environnementales, +): + if not is_open: + return no_update, no_update + + available_in_df = [col for col in OBSERVATOIRE_COLUMNS if col in df.columns] + lff = prepare_dashboard_data( + lff=df.lazy().select(available_in_df), + year=dashboard_year, + acheteur_id=dashboard_acheteur_id, + acheteur_categorie=dashboard_acheteur_categorie, + acheteur_departement_code=dashboard_acheteur_departement_code, + titulaire_id=dashboard_titulaire_id, + titulaire_categorie=dashboard_titulaire_categorie, + titulaire_departement_code=dashboard_titulaire_departement_code, + type=dashboard_marche_type, + objet=dashboard_marche_objet, + code_cpv=dashboard_marche_code_cpv, + considerations_sociales=dashboard_marche_considerations_sociales, + considerations_environnementales=dashboard_marche_considerations_environnementales, + montant_min=dashboard_montant_min, + montant_max=dashboard_montant_max, + techniques=dashboard_marche_techniques, + marche_innovant=dashboard_marche_innovant, + sous_traitance_declaree=dashboard_marche_sous_traitance_declaree, + ) + + dff = lff.collect(engine="streaming") + + table_data = dff.to_dicts() + table_columns = [ + { + "name": data_schema.get(col, {}).get("title", col), + "id": col, + "type": "text", + "format": {"nully": "N/A"}, + } + for col in available_in_df + ] + return table_data, table_columns diff --git a/src/utils.py b/src/utils.py index 15ef713..6c1c996 100644 --- a/src/utils.py +++ b/src/utils.py @@ -751,10 +751,10 @@ def prepare_dashboard_data( if code_cpv: lff = lff.filter(pl.col("codeCPV").str.starts_with(code_cpv)) - if marche_innovant != "all": + if marche_innovant and marche_innovant != "all": lff = lff.filter(pl.col("marcheInnovant") == marche_innovant) - if sous_traitance_declaree != "all": + if sous_traitance_declaree and sous_traitance_declaree != "all": lff = lff.filter(pl.col("sousTraitanceDeclaree") == sous_traitance_declaree) if techniques: