import math import dash_bootstrap_components as dbc from dash import Input, Output, State, callback, dcc, html, register_page from src.db import count_unique_marches from src.figures import DataTable from src.pages.a_propos.abonnement import abonnement_features from src.utils.cache import cache from src.utils.data import DF_ACHETEURS, DF_TITULAIRES from src.utils.search import search_org from src.utils.seo import META_CONTENT from src.utils.table import format_number, setup_table_columns NAME = "Recherche" _STAT_STYLE = {"fontWeight": "bold", "color": "var(--primary-color-text)"} @cache.memoize(timeout=12 * 60 * 60) def _tagline_stats() -> tuple[str, str, str]: nb_acheteurs = format_number(round(DF_ACHETEURS.height, -3)) nb_titulaires = format_number(round(DF_TITULAIRES.height, -3)) nb_marches_millions = math.floor(count_unique_marches() / 100_000) / 10 unite_marches = "million" if nb_marches_millions < 2 else "millions" nb_marches = f"{nb_marches_millions:.1f}".replace(".", ",") + " " + unite_marches return nb_acheteurs, nb_titulaires, nb_marches def _tagline() -> html.P: nb_acheteurs, nb_titulaires, nb_marches = _tagline_stats() return html.P( [ "Recherchez parmi ", html.Span(nb_acheteurs, style=_STAT_STYLE), " acheteurs et ", html.Span(nb_titulaires, style=_STAT_STYLE), " titulaires dans une base de données de plus de ", html.Span(nb_marches, style=_STAT_STYLE), " de marchés publics", ] ) _features_gratuites = dcc.Markdown(""" - Recherche d'acheteurs et de titulaires - [Tableau](/tableau) filtrable et personnalisable sur des dizaines de colonnes et exports Excel - Fiches détaillées : acheteur, titulaire, marché - Cartes, statistiques et [observatoire](/observatoire) national personnalisable - 100 % open source et Open Data """) home_intro = html.Div( id="home_intro", className="container", style={"maxWidth": "900px", "marginTop": "1rem"}, children=[ html.P( [ html.Span( "colibre", style={ "fontWeight": 600, "fontSize": "1.5rem", "color": "var(--primary-color-text)", }, ), " ouvre les données des marchés publics français à toutes et " "tous : recherchez, filtrez, cartographiez, téléchargez.", ], className="text-center", style={ "maxWidth": "750px", "fontSize": "1.5rem", "margin": "6rem auto 1.5rem", }, ), dbc.Row( [ dbc.Col( [html.H4("✅ Gratuit, sans compte"), _features_gratuites], md=6, style={ "borderRight": "1px solid var(--bs-border-color)", "paddingRight": "2rem", }, ), dbc.Col( [ html.H4("⭐ Avec un abonnement"), abonnement_features, dcc.Link( "En savoir plus et s'abonner", href="/a-propos/abonnement", style={"marginLeft": "20px"}, ), ], md=6, style={"paddingLeft": "2rem"}, ), ], className="align-items-start", ), ], ) register_page( __name__, path="/", title="Outils pour l'exploration des marchés publics | colibre", name=NAME, description="Explorez et analysez les données des marchés publics français. Pour une commande publique accessible à toutes et tous.", image_url=META_CONTENT["image_url"], order=0, ) def layout(**_): return html.Div( className="container", children=[ html.Div( className="tagline", children=_tagline(), ), html.Div( style={ "display": "flex", "justifyContent": "center", "marginTop": "30px", "marginBottom": "30px", }, children=[ dcc.Input( id="search", type="text", placeholder="Nom d'acheteur/entreprise, SIREN/SIRET, code département", autoFocus=True, style={ "margin": "0", "width": "500px", "border": "1px solid #ccc", "borderRight": "none", "borderRadius": "3px 0 0 3px", "padding": "5px 10px", "outline": "none", "height": "34px", }, ), html.Button( "=>", id="search-button", className="btn btn-secondary", style={ "border": "1px solid #ccc", "borderRadius": "0 3px 3px 0", "marginLeft": "0", "height": "34px", # Le padding vertical (.375rem) et le line-height # (1.5) de .btn dépassent la hauteur fixe de 34px : # le glyphe débordait vers le bas. On centre en flex # et on neutralise le padding vertical. "display": "flex", "alignItems": "center", "justifyContent": "center", "paddingTop": "0", "paddingBottom": "0", "lineHeight": "1", }, ), ], ), html.P( [ "...ou bien filtrez les marchés publics dans la vue ", dcc.Link("Tableau", href="/tableau"), ], style={"textAlign": "center"}, id="mention_tableau", ), home_intro, # html.Div( # className="search_options", # children=[dcc.RadioItems(options=["Acheteur(s)"])], # ), dbc.Row(id="search_results"), ], ) @callback( Output("search_results", "children"), Output("mention_tableau", "style"), Output("home_intro", "style"), Input("search", "n_submit"), Input("search-button", "n_clicks"), State("search", "value"), prevent_initial_call=True, ) def update_search_results(n_submit, n_clicks, query): if query and len(query) >= 1: cols = [] for org_type in ["acheteur", "titulaire"]: if org_type == "acheteur": dff = DF_ACHETEURS elif org_type == "titulaire": dff = DF_TITULAIRES else: raise ValueError(f"{org_type} is not supported") # Search acheteurs and titulaires using the same function results = search_org(dff, query, org_type=org_type) count = results.height # Format output columns, tooltip = setup_table_columns(results, hideable=False) col = ( dbc.Col( children=[ html.H3(f"{org_type.title()}s : {count}"), DataTable( dtid=f"results_{org_type}_datatable", columns=columns, data=results.to_dicts(), page_size=10, sort_action="none", filter_action="none", ), ], md=6, ) if count > 0 else html.P(f"Aucun {org_type} trouvé.") ) cols.append(col) style = {"textAlign": "center", "display": "none"} return cols, style, {"display": "none"} return html.P(""), {"textAlign": "center"}, home_intro.style