feat(titulaire): migration du tableau des marchés vers AG Grid (#41)

This commit is contained in:
Colin Maudry
2026-07-13 11:02:52 +02:00
parent 91aeca6f04
commit 7d413ccda4
+23 -158
View File
@@ -3,12 +3,10 @@ import datetime
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import polars as pl import polars as pl
from dash import ( from dash import (
ClientsideFunction,
Input, Input,
Output, Output,
State, State,
callback, callback,
clientside_callback,
dcc, dcc,
html, html,
register_page, register_page,
@@ -16,25 +14,21 @@ from dash import (
from src.db import aggregate_marches, count_marches, query_marches, schema from src.db import aggregate_marches, count_marches, query_marches, schema
from src.figures import ( from src.figures import (
DataTable,
get_distance_histogram, get_distance_histogram,
get_org_location_map, get_org_location_map,
get_top_org_table, get_top_org_ag_grid,
make_column_picker, make_column_picker,
) )
from src.utils.data import DF_TITULAIRES, get_annuaire_data, get_departement_region 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.frontend import DROPDOWN_LABELS_FR, get_button_properties
from src.utils.seo import META_CONTENT from src.utils.seo import META_CONTENT
from src.utils.table import ( from src.utils.table import (
COLUMNS, COLUMNS,
filter_table_data,
format_number, format_number,
get_default_hidden_columns, get_default_hidden_columns,
prepare_table_data,
sort_table_data,
write_styled_excel, write_styled_excel,
) )
from src.utils.tracking import track_search
def get_title(titulaire_id: str = None) -> str: 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]: 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).""" """WHERE SQL scopant les requêtes à ce titulaire (et éventuellement une année)."""
titulaire_siret = pathname.split("/")[-1] return entity_scope("titulaire", pathname.split("/")[-1], titulaire_year)
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
register_page( register_page(
@@ -67,25 +55,13 @@ register_page(
order=5, 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 = [ def layout(titulaire_id=None, **kwargs):
return [
dcc.Store(id="titulaire-hidden-columns", storage_type="local"), dcc.Store(id="titulaire-hidden-columns", storage_type="local"),
dcc.Store(id="filter-cleanup-trigger-titulaire"), 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"), dcc.Location(id="titulaire_url", refresh="callback-nav"),
html.Div( html.Div(
children=[ children=[
@@ -106,7 +82,10 @@ layout = [
), ),
html.P( html.P(
id="titulaire_activite_libelle", id="titulaire_activite_libelle",
style={"color": "gray", "marginTop": "-10px"}, style={
"color": "gray",
"marginTop": "-10px",
},
), ),
], ],
width=8, width=8,
@@ -118,7 +97,8 @@ layout = [
+ [ + [
str(year) str(year)
for year in range( for year in range(
2018, int(datetime.date.today().year) + 1 2018,
int(datetime.date.today().year) + 1,
) )
], ],
placeholder="Année", placeholder="Année",
@@ -268,12 +248,14 @@ layout = [
scrollable=True, scrollable=True,
size="xl", size="xl",
), ),
DATATABLE, html.Div(
id="titulaire-grid-container", className="marches_table"
),
], ],
), ),
], ],
), ),
] ]
@callback( @callback(
@@ -398,47 +380,6 @@ def update_download_button_titulaire(pathname, titulaire_year):
return get_button_properties(count_marches(where_sql, params)) 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( @callback(
Output(component_id="top10_acheteurs", component_property="children"), Output(component_id="top10_acheteurs", component_property="children"),
Input(component_id="titulaire_url", component_property="pathname"), Input(component_id="titulaire_url", component_property="pathname"),
@@ -446,7 +387,7 @@ def get_last_marches_data(
) )
def get_top_acheteurs(pathname, titulaire_year): def get_top_acheteurs(pathname, titulaire_year):
where_sql, params = _titulaire_scope(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"] 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") 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( @callback(
Output("titulaire-hidden-columns", "data", allow_duplicate=True), Output("titulaire-hidden-columns", "data", allow_duplicate=True),
Input("titulaire_column_list", "selected_rows"), Input("titulaire_column_list", "selected_rows"),
@@ -551,22 +436,9 @@ def update_hidden_columns_from_checkboxes(selected_columns):
return [] 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( @callback(
Output("titulaire_column_list", "selected_rows"), 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 State("titulaire_column_list", "selected_rows"), # pour éviter la boucle infinie
) )
def update_checkboxes_from_hidden_columns(hidden_cols, current_checkboxes): 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 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( @callback(
Output("titulaire-distance-histogram", "children"), Output("titulaire-distance-histogram", "children"),
Input("titulaire_url", "pathname"), 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"), html.H6("par nombre de marchés", className="card-subtitle mb-2 text-muted"),
fig, fig,
] ]
register_entity_grid_callbacks("titulaire")