From 7d413ccda49751a5322c58282ab58e0ba8f805ef Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 13 Jul 2026 11:02:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(titulaire):=20migration=20du=20tableau=20d?= =?UTF-8?q?es=20march=C3=A9s=20vers=20AG=20Grid=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/titulaire.py | 543 ++++++++++++++++------------------------- 1 file changed, 204 insertions(+), 339 deletions(-) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index f43fdac..09be209 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -3,12 +3,10 @@ import datetime import dash_bootstrap_components as dbc import polars as pl from dash import ( - ClientsideFunction, Input, Output, State, callback, - clientside_callback, dcc, html, register_page, @@ -16,25 +14,21 @@ from dash import ( from src.db import aggregate_marches, count_marches, query_marches, schema from src.figures import ( - DataTable, get_distance_histogram, get_org_location_map, - get_top_org_table, + get_top_org_ag_grid, make_column_picker, ) from src.utils.data import DF_TITULAIRES, get_annuaire_data, get_departement_region +from src.utils.entity_grid import entity_scope, register_entity_grid_callbacks from src.utils.frontend import DROPDOWN_LABELS_FR, get_button_properties from src.utils.seo import META_CONTENT from src.utils.table import ( COLUMNS, - filter_table_data, format_number, get_default_hidden_columns, - prepare_table_data, - sort_table_data, write_styled_excel, ) -from src.utils.tracking import track_search def get_title(titulaire_id: str = None) -> str: @@ -48,13 +42,7 @@ def get_title(titulaire_id: str = None) -> str: def _titulaire_scope(pathname: str, titulaire_year: str | None) -> tuple[str, list]: """WHERE SQL scopant les requêtes à ce titulaire (et éventuellement une année).""" - titulaire_siret = pathname.split("/")[-1] - where_sql = "titulaire_id = ? AND titulaire_typeIdentifiant = 'SIRET'" - params: list = [titulaire_siret] - if titulaire_year and titulaire_year != "Toutes les années": - where_sql += ' AND YEAR("dateNotification") = ?' - params.append(int(titulaire_year)) - return where_sql, params + return entity_scope("titulaire", pathname.split("/")[-1], titulaire_year) register_page( @@ -67,213 +55,207 @@ register_page( order=5, ) -DATATABLE = html.Div( - className="marches_table", - children=DataTable( - dtid="titulaire_datatable", - persistence=True, - persistence_type="local", - persisted_props=["filter_query", "sort_by"], - page_action="custom", - filter_action="custom", - sort_action="custom", - page_size=10, - hidden_columns=[], - columns=[{"id": col, "name": col} for col in schema.names()], - ), -) -layout = [ - dcc.Store(id="titulaire-hidden-columns", storage_type="local"), - dcc.Store(id="filter-cleanup-trigger-titulaire"), - dcc.Location(id="titulaire_url", refresh="callback-nav"), - html.Div( - children=[ - html.Div( - style={"marginBottom": "50px"}, - children=[ - dbc.Row( - className="mb-2", - children=[ - dbc.Col( - [ - html.H2( - children=[ - html.Span(id="titulaire_siret"), - " - ", - html.Span(id="titulaire_nom"), - ], - ), - html.P( - id="titulaire_activite_libelle", - style={"color": "gray", "marginTop": "-10px"}, - ), - ], - width=8, - ), - dbc.Col( - dcc.Dropdown( - id="titulaire_year", - options=["Toutes les années"] - + [ - str(year) - for year in range( - 2018, int(datetime.date.today().year) + 1 - ) - ], - placeholder="Année", - labels=DROPDOWN_LABELS_FR, - ), - width=4, - ), - ], - ), - dbc.Row( - className="mb-2", - children=[ - dbc.Col( - className="org_infos", - children=[ - # TODO: ajouter le type d'acheteur : commune, CD, CR, etc. - html.P( - [ - "Commune : ", - html.Strong(id="titulaire_commune"), - ] - ), - html.P( - [ - "Département : ", - html.Strong(id="titulaire_departement"), - ] - ), - html.P( - [ - "Région : ", - html.Strong(id="titulaire_region"), - ] - ), - html.A( - id="titulaire_lien_annuaire", - children="Plus de détails sur l'Annuaire des entreprises", - ), - ], - width=4, - ), - dbc.Col( - children=[ - html.P(id="titulaire_titre_stats"), - html.P(id="titulaire_marches_remportes"), - html.P(id="titulaire_acheteurs_differents"), - html.Button( - "Téléchargement au format Excel", - id="btn-download-data-titulaire", - className="btn btn-primary", - ), - dcc.Download(id="download-data-titulaire"), - ], - width=4, - ), - dbc.Col( - id="titulaire_map", - width=4, - style={"minHeight": "300px"}, - ), - ], - ), - dbc.Row( - children=[ - dbc.Col( - html.Div( - children=[ - html.H3("Top acheteurs"), - html.Div( - className="marches_table", - id="top10_acheteurs", - style={"minHeight": "420px"}, +def layout(titulaire_id=None, **kwargs): + return [ + dcc.Store(id="titulaire-hidden-columns", storage_type="local"), + dcc.Store(id="titulaire-total"), + dcc.Store(id="titulaire-total-unique"), + dcc.Store(id="entity-grid-columns-state", storage_type="local"), + dcc.Location(id="titulaire_url", refresh="callback-nav"), + html.Div( + children=[ + html.Div( + style={"marginBottom": "50px"}, + children=[ + dbc.Row( + className="mb-2", + children=[ + dbc.Col( + [ + html.H2( + children=[ + html.Span(id="titulaire_siret"), + " - ", + html.Span(id="titulaire_nom"), + ], + ), + html.P( + id="titulaire_activite_libelle", + style={ + "color": "gray", + "marginTop": "-10px", + }, ), ], + width=8, ), - width=8, - ), - dbc.Col( - id="titulaire-distance-histogram", - width=4, - style={"minHeight": "450px"}, - ), - ], - ), - ], - ), - # récupérer les données de l'acheteur sur l'api annuaire - html.H3("Derniers marchés publics remportés"), - dcc.Loading( - overlay_style={"visibility": "visible", "filter": "blur(2px)"}, - id="loading-home", - type="default", - children=[ - html.Div( - [ - dbc.Button( - "Colonnes", - id="titulaire_columns_open", - color="secondary", - size="sm", - className="column_list", - ), - dbc.Button( - "Téléchargement désactivé au-delà de 65 000 lignes", - id="btn-download-filtered-data-titulaire", - color="secondary", - size="sm", - disabled=True, - ), - dcc.Download(id="titulaire-download-filtered-data"), - dbc.Button( - "Réinitialiser", - title="Supprime tous les filtres et les tris. Autrement ils sont conservés même si vous fermez la page.", - id="btn-titulaire-reset", - color="danger", - outline=True, - size="sm", - ), - ], - className="table-toolbar", - ), - html.Div( - html.Span(id="titulaire_nb_rows"), - className="table-meta", - ), - dbc.Modal( - [ - dbc.ModalHeader( - dbc.ModalTitle("Choix des colonnes à afficher") - ), - dbc.ModalBody( - id="titulaire_columns_body", - children=make_column_picker("titulaire"), - ), - dbc.ModalFooter( + dbc.Col( + dcc.Dropdown( + id="titulaire_year", + options=["Toutes les années"] + + [ + str(year) + for year in range( + 2018, + int(datetime.date.today().year) + 1, + ) + ], + placeholder="Année", + labels=DROPDOWN_LABELS_FR, + ), + width=4, + ), + ], + ), + dbc.Row( + className="mb-2", + children=[ + dbc.Col( + className="org_infos", + children=[ + # TODO: ajouter le type d'acheteur : commune, CD, CR, etc. + html.P( + [ + "Commune : ", + html.Strong(id="titulaire_commune"), + ] + ), + html.P( + [ + "Département : ", + html.Strong(id="titulaire_departement"), + ] + ), + html.P( + [ + "Région : ", + html.Strong(id="titulaire_region"), + ] + ), + html.A( + id="titulaire_lien_annuaire", + children="Plus de détails sur l'Annuaire des entreprises", + ), + ], + width=4, + ), + dbc.Col( + children=[ + html.P(id="titulaire_titre_stats"), + html.P(id="titulaire_marches_remportes"), + html.P(id="titulaire_acheteurs_differents"), + html.Button( + "Téléchargement au format Excel", + id="btn-download-data-titulaire", + className="btn btn-primary", + ), + dcc.Download(id="download-data-titulaire"), + ], + width=4, + ), + dbc.Col( + id="titulaire_map", + width=4, + style={"minHeight": "300px"}, + ), + ], + ), + dbc.Row( + children=[ + dbc.Col( + html.Div( + children=[ + html.H3("Top acheteurs"), + html.Div( + className="marches_table", + id="top10_acheteurs", + style={"minHeight": "420px"}, + ), + ], + ), + width=8, + ), + dbc.Col( + id="titulaire-distance-histogram", + width=4, + style={"minHeight": "450px"}, + ), + ], + ), + ], + ), + # récupérer les données de l'acheteur sur l'api annuaire + html.H3("Derniers marchés publics remportés"), + dcc.Loading( + overlay_style={"visibility": "visible", "filter": "blur(2px)"}, + id="loading-home", + type="default", + children=[ + html.Div( + [ dbc.Button( - "Fermer", - id="titulaire_columns_close", - className="ms-auto", - n_clicks=0, - ) - ), - ], - id="titulaire_columns", - is_open=False, - fullscreen="md-down", - scrollable=True, - size="xl", - ), - DATATABLE, - ], - ), - ], - ), -] + "Colonnes", + id="titulaire_columns_open", + color="secondary", + size="sm", + className="column_list", + ), + dbc.Button( + "Téléchargement désactivé au-delà de 65 000 lignes", + id="btn-download-filtered-data-titulaire", + color="secondary", + size="sm", + disabled=True, + ), + dcc.Download(id="titulaire-download-filtered-data"), + dbc.Button( + "Réinitialiser", + title="Supprime tous les filtres et les tris. Autrement ils sont conservés même si vous fermez la page.", + id="btn-titulaire-reset", + color="danger", + outline=True, + size="sm", + ), + ], + className="table-toolbar", + ), + html.Div( + html.Span(id="titulaire_nb_rows"), + className="table-meta", + ), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Choix des colonnes à afficher") + ), + dbc.ModalBody( + id="titulaire_columns_body", + children=make_column_picker("titulaire"), + ), + dbc.ModalFooter( + dbc.Button( + "Fermer", + id="titulaire_columns_close", + className="ms-auto", + n_clicks=0, + ) + ), + ], + id="titulaire_columns", + is_open=False, + fullscreen="md-down", + scrollable=True, + size="xl", + ), + html.Div( + id="titulaire-grid-container", className="marches_table" + ), + ], + ), + ], + ), + ] @callback( @@ -398,47 +380,6 @@ def update_download_button_titulaire(pathname, titulaire_year): return get_button_properties(count_marches(where_sql, params)) -@callback( - Output("titulaire_datatable", "data"), - Output("titulaire_datatable", "columns"), - Output("titulaire_datatable", "tooltip_header"), - Output("titulaire_datatable", "data_timestamp"), - Output("titulaire_nb_rows", "children"), - Output("btn-download-filtered-data-titulaire", "disabled"), - Output("btn-download-filtered-data-titulaire", "children"), - Output("btn-download-filtered-data-titulaire", "title"), - Output("filter-cleanup-trigger-titulaire", "data"), - Input("titulaire_url", "pathname"), - Input("titulaire_year", "value"), - Input("titulaire_datatable", "page_current"), - Input("titulaire_datatable", "page_size"), - Input("titulaire_datatable", "filter_query"), - Input("titulaire_datatable", "sort_by"), - State("titulaire_datatable", "data_timestamp"), -) -def get_last_marches_data( - pathname, - titulaire_year, - page_current, - page_size, - filter_query, - sort_by, - data_timestamp, -) -> list[dict]: - where_sql, params = _titulaire_scope(pathname, titulaire_year) - return prepare_table_data( - None, - data_timestamp, - filter_query, - page_current, - page_size, - sort_by, - "titulaire", - base_where_sql=where_sql, - base_params=params, - ) - - @callback( Output(component_id="top10_acheteurs", component_property="children"), Input(component_id="titulaire_url", component_property="pathname"), @@ -446,7 +387,7 @@ def get_last_marches_data( ) def get_top_acheteurs(pathname, titulaire_year): where_sql, params = _titulaire_scope(pathname, titulaire_year) - return get_top_org_table( + return get_top_org_ag_grid( query_marches(where_sql, params).lazy(), "acheteur", ["titulaire_distance"] ) @@ -481,62 +422,6 @@ def download_titulaire_data( return dcc.send_bytes(to_bytes, filename=f"decp_{titulaire_nom}_{date}.xlsx") -@callback( - Output("titulaire-download-filtered-data", "data"), - Input("btn-download-filtered-data-titulaire", "n_clicks"), - State("titulaire_url", "pathname"), - State("titulaire_year", "value"), - State("titulaire_nom", "children"), - State("titulaire_datatable", "filter_query"), - State("titulaire_datatable", "sort_by"), - State("titulaire_datatable", "hidden_columns"), - prevent_initial_call=True, -) -def download_filtered_titulaire_data( - n_clicks, - pathname, - titulaire_year, - titulaire_nom, - filter_query, - sort_by, - hidden_columns: list | None = None, -): - where_sql, params = _titulaire_scope(pathname, titulaire_year) - lff: pl.LazyFrame = query_marches(where_sql, params).lazy() - - # Les colonnes masquées sont supprimées - if hidden_columns: - lff = lff.drop(hidden_columns) - - if filter_query: - track_search(filter_query, "titu download") - lff = filter_table_data(lff, filter_query) - - if len(sort_by) > 0: - lff = sort_table_data(lff, sort_by) - - def to_bytes(buffer): - write_styled_excel(lff.collect(engine="streaming"), buffer) - - date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") - return dcc.send_bytes( - to_bytes, filename=f"decp_filtrées_{titulaire_nom}_{date}.xlsx" - ) - - -# Pour nettoyer les icontains et i< des filtres -# voir aussi src/assets/dash_clientside.js -clientside_callback( - ClientsideFunction( - namespace="clientside", - function_name="clean_filters", - ), - Output("filter-cleanup-trigger-titulaire", "data", allow_duplicate=True), - Input("filter-cleanup-trigger-titulaire", "data"), - prevent_initial_call=True, -) - - @callback( Output("titulaire-hidden-columns", "data", allow_duplicate=True), Input("titulaire_column_list", "selected_rows"), @@ -551,22 +436,9 @@ def update_hidden_columns_from_checkboxes(selected_columns): return [] -@callback( - Output("titulaire_datatable", "hidden_columns"), - Input( - "titulaire-hidden-columns", - "data", - ), -) -def store_hidden_columns(hidden_columns): - if hidden_columns is None: - hidden_columns = get_default_hidden_columns("titulaire") - return hidden_columns - - @callback( Output("titulaire_column_list", "selected_rows"), - Input("titulaire_datatable", "hidden_columns"), + Input("titulaire-hidden-columns", "data"), State("titulaire_column_list", "selected_rows"), # pour éviter la boucle infinie ) def update_checkboxes_from_hidden_columns(hidden_cols, current_checkboxes): @@ -589,16 +461,6 @@ def toggle_titulaire_columns(click_open, click_close, is_open): return is_open -@callback( - Output("titulaire_datatable", "filter_query", allow_duplicate=True), - Output("titulaire_datatable", "sort_by"), - Input("btn-titulaire-reset", "n_clicks"), - prevent_initial_call=True, -) -def reset_view(n_clicks): - return "", [] - - @callback( Output("titulaire-distance-histogram", "children"), Input("titulaire_url", "pathname"), @@ -617,3 +479,6 @@ def update_titulaire_distance_histogram(pathname, titulaire_year): html.H6("par nombre de marchés", className="card-subtitle mb-2 text-muted"), fig, ] + + +register_entity_grid_callbacks("titulaire")