import pandas as pd
import streamlit as st
import datetime as dt
import plotly.graph_objects as go
from pandas.tseries.offsets import MonthEnd
import plotly.express as px
import numpy as np
from modules.Funciones import COLORS,PICTON_BLUE, render_metric,fmt_currency,apply_cabanna_theme,_render_var_card,st_card,_fmt_range,_safe_pct
import io
from modules.drive import load_comensales, load_folios,FOLDER_ID_FOLIOS,FILE_ID
from modules.api_client import api_get
import locale
import unicodedata

# ======================================================================
# 5) FORMATO EJECUTIVO: LABELS DE FECHA Y DINERO
# ======================================================================
MES_MAP = {1:"Ene",2:"Feb",3:"Mar",4:"Abr",5:"May",6:"Jun",7:"Jul",8:"Ago",9:"Sep",10:"Oct",11:"Nov",12:"Dic"}  # Meses en español (abreviados).

DOW_MAP = {0:"Lunes",1:"Martes",2:"Miércoles",3:"Jueves",4:"Viernes",5:"Sábado",6:"Domingo"}

def fmt_money_abbrev(x: float) -> str:
    
    """
    Formatea una cantidad monetaria en notación de negocio:
    - 25,000 -> $25.0K
    - 2,500,000 -> $2.5M
    - 1,200,000,000 -> $1.2B

    Objetivo UX:
    - Evitar ejes ilegibles con números largos y acelerar lectura ejecutiva.
    """
    # Manejo nulos/NaN.
    if x is None or (isinstance(x, float) and np.isnan(x)):                 
        return ""
    ax = abs(x)                                                              
    sign = "-" if x < 0 else ""                                              

    if ax >= 1_000_000_000:                                                  
        return f"{sign}${ax/1_000_000_000:.1f}B"
    if ax >= 1_000_000:                                                     
        return f"{sign}${ax/1_000_000:.1f}M"
    if ax >= 1_000:                                                        
        return f"{sign}${ax/1_000:.1f}K"
    return f"{sign}${ax:.0f}"                                                

def fmt_month_label(ts: pd.Timestamp) -> str:
    """
    Convierte un Timestamp de inicio de mes a etiqueta ejecutiva:
    - 2025-01-01 -> "Ene '25"

    Objetivo UX:
    - Evitar formatos técnicos en eje X y mantener consistencia visual.
    """
    return f"{MES_MAP[ts.month]} '{str(ts.year)[-2:]}"                        # Abrev mes + año 2 dígitos.

def fmt_fecha_es(ts: pd.Timestamp) -> str:
    ts = pd.to_datetime(ts)
    return f"{DOW_MAP[ts.weekday()]} {ts.day:02d}, {MES_MAP[ts.month]} {ts.year}"

def safe_pct(curr, prev):
    if prev is None or pd.isna(prev) or prev == 0:
        return np.nan
    return (curr / prev) - 1

def build_df_tickets(df_folios: pd.DataFrame) -> pd.DataFrame:
    """
    Convierte ventas a nivel línea (df_folios) a nivel ticket (df_tickets).
    ticket_id robusto = NombreSucursal|Estacion|Folio
    Devuelve un DF con 1 fila por ticket: fecha_ticket, ingreso_ticket, productos_ticket.
    """
    df = df_folios.copy()

    df["Fecha"] = pd.to_datetime(df["Fecha"], errors="coerce")
    df["Ingreso_linea"] = df['ventas_subtotal']



    df["ticket_id"] = (
        df["NombreSucursal"].astype(str).str.strip() + "|" +
        df["Folio"].astype(str).str.strip()
    )
    # 1 ticket = 1 fila
    df_tickets = (
        df.groupby("ticket_id", as_index=False)
        .agg(
            fecha_ticket=("Fecha", "min"),           # fecha del ticket (primera línea)
            ingreso_ticket=("Ingreso_linea", "sum"), # total del ticket

        )
    )

    df_tickets["fecha_ticket"] = pd.to_datetime(df_tickets["fecha_ticket"], errors="coerce")
    df_tickets["Mes"] = df_tickets["fecha_ticket"].dt.to_period("M").dt.to_timestamp()

    return df_tickets

def fmt_delta_pct_or_abs(curr, prev, kind="currency"):
    """Delta para st.metric: prefiere %; si no hay base, usa abs; si no hay nada, None."""
    if prev is None or pd.isna(prev):
        return None

    pct = safe_pct(curr, prev)
    if not pd.isna(pct):
        sign = "+" if pct >= 0 else ""
        return f"{sign}{pct*100:,.1f}% vs anterior"

    # fallback abs
    absd = curr - prev
    sign = "+" if absd >= 0 else ""
    if kind == "currency":
        return f"{sign}${absd:,.0f} vs anterior"
    return f"{sign}{absd:,.0f} vs anterior"

def ticket_metrics(df_tickets: pd.DataFrame, mes_sel: pd.Timestamp):
    """
    Calcula KPIs de tickets:
    - Ticket promedio anual (YTD del año de mes_sel) y delta vs año anterior (YTD)
    - Ticket promedio del mes_sel y delta vs mes anterior
    - Tickets totales del mes_sel y delta vs mes anterior
    - Ticket mínimo del mes_sel y cuántos tickets tienen ese mínimo
    - Ticket máximo del mes_sel y fecha exacta (Lunes 05, marzo 2025)
    """
    mes_sel = pd.to_datetime(mes_sel).normalize().replace(day=1)
    year = mes_sel.year

    # ----- Mes actual y mes anterior -----
    df_m = df_tickets[df_tickets["Mes"] == mes_sel].copy()
    mes_prev = (mes_sel - pd.offsets.MonthBegin(1)).normalize().replace(day=1)
    df_prev = df_tickets[df_tickets["Mes"] == mes_prev].copy()

    # ----- Ticket promedio mensual + delta MoM -----
    ticket_avg_mes = float(df_m["ingreso_ticket"].mean()) if len(df_m) else np.nan
    ticket_avg_prev = float(df_prev["ingreso_ticket"].mean()) if len(df_prev) else np.nan

    # ----- Tickets totales mes + delta MoM -----
    n_tickets_mes = int(df_m["ticket_id"].nunique()) if len(df_m) else 0
    n_tickets_prev = int(df_prev["ticket_id"].nunique()) if len(df_prev) else 0

    # ----- Ticket min + cuántos tienen ese mínimo -----
    if len(df_m):
        ticket_min = float(df_m["ingreso_ticket"].min())
        n_ticket_min = int((df_m["ingreso_ticket"] == ticket_min).sum())
    else:
        ticket_min, n_ticket_min = np.nan, 0

    # ----- Ticket max + fecha exacta -----
    if len(df_m):
        idx_max = df_m["ingreso_ticket"].idxmax()
        ticket_max = float(df_m.loc[idx_max, "ingreso_ticket"])
        fecha_max = df_m.loc[idx_max, "fecha_ticket"]
        fecha_max_str = fmt_fecha_es(fecha_max)
    else:
        ticket_max, fecha_max_str = np.nan, "Sin datos"

    # ----- Ticket promedio anual (YTD) y delta vs año anterior -----
    # YTD: de enero a mes_sel dentro del mismo año
    ytd_start = pd.Timestamp(year=year, month=1, day=1)
    ytd_end = (mes_sel + pd.offsets.MonthBegin(1))  # exclusivo: inicio del siguiente mes

    df_ytd = df_tickets[(df_tickets["fecha_ticket"] >= ytd_start) & (df_tickets["fecha_ticket"] < ytd_end)]
    ticket_avg_ytd = float(df_ytd["ingreso_ticket"].mean()) if len(df_ytd) else np.nan

    # YTD año anterior (mismo corte de meses)
    ytd_start_prev = pd.Timestamp(year=year-1, month=1, day=1)
    ytd_end_prev = pd.Timestamp(year=year-1, month=mes_sel.month, day=1) + pd.offsets.MonthBegin(1)
    df_ytd_prev = df_tickets[(df_tickets["fecha_ticket"] >= ytd_start_prev) & (df_tickets["fecha_ticket"] < ytd_end_prev)]
    ticket_avg_ytd_prev = float(df_ytd_prev["ingreso_ticket"].mean()) if len(df_ytd_prev) else np.nan



    return {
        "ticket_avg_ytd": ticket_avg_ytd,
        "ticket_avg_ytd_prev": ticket_avg_ytd_prev,

        "ticket_avg_mes": ticket_avg_mes,
        "ticket_avg_prev": ticket_avg_prev,

        "n_tickets_mes": n_tickets_mes,
        "n_tickets_prev": n_tickets_prev,

        "ticket_min": ticket_min,
        "n_ticket_min": n_ticket_min,

        "ticket_max": ticket_max,
        "ticket_max_fecha": fecha_max_str,



    }

# Función principal
def render_analisis_general():
 
    # MIN / MAX de Fecha (viene de API)
    # mm esperado: {"fecha_min":"YYYY-MM-DD", "fecha_max":"YYYY-MM-DD"}
    mm = api_get("/ventas/minmax")
    fecha_min = pd.to_datetime(mm["fecha_min"])
    fecha_max = pd.to_datetime(mm["fecha_max"])

    # Guardrails por si la API viene rara
    if pd.isna(fecha_min) or pd.isna(fecha_max):
        st.error("No se pudo interpretar fecha_min/fecha_max desde la API.")
        st.stop()

    # Selector de año (limitado desde 2017 por decisión de análisis)
    anios_disponibles = list(range(2017, fecha_max.year + 1))
    anio_actual = dt.datetime.now().year

    col_titulo, col_selector = st.columns([3, 1], vertical_alignment="bottom") 

    with col_titulo:
        st.markdown("## Análisis general Cabanna")
        st.caption(
            f"Datos disponibles de {fecha_min.year} a {fecha_max.year}. "
            f"El análisis principal está optimizado a partir de 2017; "
            f"periodos anteriores requieren un análisis histórico especializado."
        )

    with col_selector:
        # El label_visibility="collapsed" oculta la etiqueta "Año" para que se vea limpio
        anio = st.selectbox(
            "Seleccionar Año", 
            options=anios_disponibles, 
            index=anios_disponibles.index(anio_actual) if anio_actual in anios_disponibles else 0,
            label_visibility="collapsed" 
        )

    # Definir año previo y rango de extracción (cargamos anio y anio-1 si existe)
    min_year = fecha_min.year
    max_year = fecha_max.year

    anio_prev = (anio - 1) if (anio - 1) >= min_year else None
    anio_ini  = max(anio - 1, min_year)

    ini_q = dt.datetime(anio_ini, 1, 1)
    fin_q = dt.datetime(anio + 1, 1, 1)

    df_folios = load_folios(ini_q, fin_q)

    if df_folios is None or df_folios.empty:
        st.error(
            "**Sin datos para el rango seleccionado**\n\n"
            f"Rango: **{ini_q} → {fin_q}**\n\n"
            "Acción sugerida:\n"
            "- Verifica que `/etl/folios` haya cargado ese periodo.\n"
            "- Revisa si la API está devolviendo 0 filas para ese rango."
        )
        st.stop()

    # Column pruning inmediato (reduce RAM y acelera operaciones siguientes)
    cols_needed = ["Fecha","Folio" ,"ventas_subtotal", "NombreSucursal"]
    df_folios = df_folios.loc[:, cols_needed]

    # Limpieza mínima de sucursal
    # - Filtramos NombreSucursal vacío / NaN / espacios
    ns = df_folios["NombreSucursal"]
    mask_ns = ns.notna() & (ns.astype(str).str.strip() != "")
    df_folios = df_folios.loc[mask_ns]

    # Tipos una sola vez
    # Fecha -> datetime (coerce a NaT si viene sucia)
    df_folios["Fecha"] = pd.to_datetime(df_folios["Fecha"], errors="coerce")

    # ventas_subtotal -> float (coerce a NaN, luego fill 0)
    # Nota RAM/CPU: pd.to_numeric sobre la columna completa una vez (mejor que por subsets)
    df_folios["ventas_subtotal"] = pd.to_numeric(df_folios["ventas_subtotal"], errors="coerce").fillna(0.0)

    fuente = "Base de datos en servidores Cabanna"

    # Definir periodo actual: año completo o YTD si es el más reciente
    ini_cur = dt.datetime(anio, 1, 1)

    if anio == max_year:
        # YTD hasta la fecha máxima disponible (más robusto que hoy)
        fin_real = min(dt.date.today(), fecha_max.date())
        fin_cur = dt.datetime(fin_real.year, fin_real.month, fin_real.day) + dt.timedelta(days=1)
        modo_comp = "YTD"
    else:
        fin_cur = dt.datetime(anio + 1, 1, 1)
        modo_comp = "Año completo"

    # Ventas del periodo actual (sumar sin crear df_cur)
    # Nota: .between no aplica directo por ser semi-open interval; usamos >= y <
    f = df_folios["Fecha"]
    v = df_folios["ventas_subtotal"]

    mask_cur = (f >= ini_cur) & (f < fin_cur)
    ventas_total = float(v.loc[mask_cur].sum())

    # ================ Ventas año anteiror ============================================== 
    ventas_prev = None
    if anio_prev is not None:
        ini_prev = dt.datetime(anio - 1, 1, 1)

        if modo_comp == "YTD":
            # Ajuste por fechas inválidas (29/30/31) del año anterior
            try:
                fin_prev_day = dt.date(anio - 1, fin_real.month, fin_real.day)
            except ValueError:
                fin_prev_day = dt.date(anio - 1, fin_real.month, 28)

            fin_prev = dt.datetime(fin_prev_day.year, fin_prev_day.month, fin_prev_day.day) + dt.timedelta(days=1)
        else:
            fin_prev = dt.datetime(anio, 1, 1)

        mask_prev = (f >= ini_prev) & (f < fin_prev)
        ventas_prev = float(v.loc[mask_prev].sum())

    # 11) Delta porcentual vs año previo (si hay base > 0)
    pct_vs_prev = None
    if (ventas_prev is not None) and (ventas_prev > 0):
        pct_vs_prev = (ventas_total - ventas_prev) / ventas_prev

    delta_str = "NA" if pct_vs_prev is None else f"{pct_vs_prev:+.2%}"
    # ==============================================================================================================================
    # KPIS
    # ==============================================================================================================================

        # ---------- Fila 1: Totales ----------
    c1,c5 = st.columns(2)

    with c1:
        st.metric(
            label="Ventas (SubTotal)",
            value=fmt_currency(ventas_total),
            delta=delta_str,
            help="Suma de Cant × PU (sin deducir descuentos ni impuestos). Delta vs periodo anterior comparable."
        )
    # with c2:
    #     render_metric(
    #         "Impuesto total",
    #         fmt_currency(impuesto_total),
    #         help_text="Impuesto cobrado por cuenta del SAT (16% cuando Impto = 1600)."
    #     )

    # with c3:
    #     render_metric(
    #         "Descuento total",
    #         fmt_currency(descuento_total),
    #         help_text="Suma de descuentos aplicados en el periodo."
    #     )
    # with c4:
    #     render_metric(
    #         "Ingreso total", 
    #         fmt_currency(ingreso_total), 
    #         help_text="Ingreso = SubTotal - Impuesto - Descuento")
        

    # -------------------------------------------------
    # Cutoff de meses completos para promedio mensual
    # -------------------------------------------------
    if anio == max_year:
        # último día del mes de fin_real
        last_day_of_month = (
            dt.date(fin_real.year, fin_real.month, 1)
            + dt.timedelta(days=32)
        ).replace(day=1) - dt.timedelta(days=1)

        # Si el mes está completo, incluimos el mes completo y cortamos al 1ro del sig. mes.
        # Si NO está completo, cortamos al 1ro del mes actual (excluye mes en curso).
        if fin_real == last_day_of_month:
            cutoff_month = (dt.date(fin_real.year, fin_real.month, 1) + dt.timedelta(days=32)).replace(day=1)
        else:
            cutoff_month = dt.date(fin_real.year, fin_real.month, 1)

        cutoff_month_dt = dt.datetime(cutoff_month.year, cutoff_month.month, cutoff_month.day)
    else:
        cutoff_month_dt = dt.datetime(anio + 1, 1, 1)


    if mask_cur.any():
        # MonthKey: mes truncado (datetime64[M]) -> liviano y rápido
        month_key = f.loc[mask_cur].values.astype("datetime64[M]")

        # Construimos un DF mínimo SOLO con lo necesario para agrupar
        tmp = pd.DataFrame({
            "mes": month_key,
            "ingreso_folio": v.loc[mask_cur].to_numpy(copy=False)  # evita copia si posible
        })

        ingreso_mensual = tmp.groupby("mes", as_index=False)["ingreso_folio"].sum()

        meses_incluidos = int(ingreso_mensual.shape[0])
        ingreso_prom_mensual = float(ingreso_mensual["ingreso_folio"].mean()) if meses_incluidos > 0 else 0.0
    else:
        ingreso_mensual = None
        meses_incluidos = 0
        ingreso_prom_mensual = 0.0

    with c5:
        render_metric(
            "Ingreso promedio mensual",
            fmt_currency(ingreso_prom_mensual) if meses_incluidos > 0 else "Mes en curso",
            help_text=(
                f"Promedio del ingreso mensual considerando solo meses completos. Meses incluidos: {meses_incluidos}."
                if meses_incluidos > 0
                else "El mes actual aún no está completo, por lo que no se calcula el promedio mensual."
            ),
        )

    
    # (Opcional) Mostrar periodos comparados para transparencia
    if ventas_prev is not None:
        st.caption(
            f"Periodo anterior: {ini_prev:%d/%m/%Y} → {(fin_prev - dt.timedelta(days=1)):%d/%m/%Y} - \n"
            f"Periodo actual: {ini_cur:%d/%m/%Y} → {(fin_cur - dt.timedelta(days=1)):%d/%m/%Y}"
        )
    else:
        st.caption(
            f"Periodo anterior: NA (primer año disponible) - \n"
            f"Periodo actual: {ini_cur:%d/%m/%Y} → {(fin_cur - dt.timedelta(days=1)):%d/%m/%Y}"
        )
    st.markdown("---")

    # ==============================================================================================================================
    # Tendencias
    # ==============================================================================================================================

    df_ventas_diarias = (
        df_folios.loc[mask_cur, ["Fecha", "ventas_subtotal"]]
        .groupby("Fecha", as_index=False)["ventas_subtotal"]
        .sum()
        .rename(columns={"ventas_subtotal": "ventas_dia"})
        .sort_values("Fecha")
        .reset_index(drop=True)
    )

    # Corte (última fecha disponible en el periodo actual)
    corte = df_ventas_diarias["Fecha"].max() if not df_ventas_diarias.empty else None
    
    # Ordenear por Fecha
    df_ventas_diarias = df_ventas_diarias.sort_values("Fecha").reset_index(drop=True)
    min_date = df_ventas_diarias['Fecha'].min()
    max_date = df_ventas_diarias['Fecha'].max()

    # Creamos una serie de 15 puntos exactos entre el inicio y el fin
    puntos_eje = pd.date_range(start=min_date, end=max_date, periods=15)


    # 3. Creamos las etiquetas formateadas
    # Formato: "05-Ene"
    textos_eje = [f"{d.day:02d}-{MES_MAP[d.month]}" for d in puntos_eje]

    # Division de pantalla
    col1, col2 = st.columns([2, 1])

    # ------------------------------ Columna 1: Línea diaria ------------------------------
    with col1:
        # 1. Al final, cuando pintes el gráfico en Streamlit
        if df_ventas_diarias.empty:
            with st.markdown("### Ventas diarias (SubTotal)"):
                st.info("No hay datos diarios para graficar.")
        else:
            # Media móvil 7 días (min_periods=1 para no perder los primeros días)
            df_ventas_diarias["ma7"] = (
                df_ventas_diarias["ventas_dia"]
                .rolling(window=7, min_periods=1)
                .mean()
            )

            # Gráfica: 2 líneas (ventas diaria y MA7) con Picton Blue
            fig_line = go.Figure()
            # Creamos la lista de fechas formateadas (ej: "15-Ene-2026")
            fechas_formateadas_es = [
                f"{d.day:02d}-{MES_MAP[d.month]}-{d.year}" 
                for d in df_ventas_diarias["Fecha"]
            ]
            # -----------------------
            # Serie real (Ventas diarias)
            # -----------------------
            fig_line.add_trace(go.Scatter(
                x=df_ventas_diarias["Fecha"],
                y=df_ventas_diarias["ventas_dia"],
                mode="lines+markers",
                name="Ventas diarias",
                # Pasamos nuestra lista personalizada al canal auxiliar 'customdata'
                customdata=fechas_formateadas_es,
                # %{customdata} jala el valor correspondiente de la lista que acabamos de pasar
                hovertemplate="Fecha: %{customdata}<br><b>Ventas: %{text}</b><extra></extra>",
                text=[fmt_money_abbrev(v) for v in df_ventas_diarias["ventas_dia"]],
                line=dict(width=2.5, color=PICTON_BLUE[5]),
                marker=dict(size=6, color=PICTON_BLUE[5]),
            ))

            # --------------------------------
            # Trendline (MA 7 días) estilizada
            # --------------------------------
            fig_line.add_trace(go.Scatter(
                x=df_ventas_diarias["Fecha"],
                y=df_ventas_diarias["ma7"],
                mode="lines",
                name="Media móvil 7 días",
                customdata=fechas_formateadas_es,
                line=dict(width=2, color=PICTON_BLUE[3], dash="dash"),
                hovertemplate="Fecha: %{customdata}<br><b>MA7: %{text}</b><extra></extra>",
                text=[fmt_money_abbrev(v) for v in df_ventas_diarias["ma7"]],
                opacity=0.65
            ))
            # -----------------------
            # Highlights: max/min
            # -----------------------
            if len(df_ventas_diarias) > 0:
                idx_max = df_ventas_diarias["ventas_dia"].idxmax()
                idx_min = df_ventas_diarias["ventas_dia"].idxmin()

                for idx, tag in [(idx_max, "Máximo"), (idx_min, "Mínimo")]:
                    fig_line.add_trace(go.Scatter(
                        x=[df_ventas_diarias.loc[idx, "Fecha"]],
                        y=[df_ventas_diarias.loc[idx, "ventas_dia"]],
                        mode="markers+text",
                        name=tag,
                        text=[f"{tag}: {fmt_money_abbrev(df_ventas_diarias.loc[idx,'ventas_dia'])}"],
                        textposition="top center",
                        showlegend=False,
                        hoverinfo="skip"
                    ))

            # -----------------------
            # Eje Y: ticks abreviados
            # -----------------------
            y_max = float(np.nanmax(df_ventas_diarias["ventas_dia"])) if len(df_ventas_diarias["ventas_dia"]) else 0
            tickvals = np.array([0, y_max*0.25, y_max*0.5, y_max*0.75, y_max])
            tickvals = np.unique(np.round(tickvals, 0))
            ticktext = [fmt_money_abbrev(v) for v in tickvals]

            fig_line.update_yaxes(
                tickvals=tickvals.tolist(),
                ticktext=ticktext,
                ticks="outside",
                ticklabelposition="outside"
            )

            # -----------------------
            # Eje X: formato de fecha
            # -----------------------

            fig_line.update_xaxes(
                tickvals=puntos_eje,    
                ticktext=textos_eje,   
            )

            # -----------------------
            # Footer: Fuente + Corte
            # -----------------------
            corte_str = f"{corte.day:02d}-{corte.strftime('%b')}-{corte.year}"
            fig_line.update_layout(
                title="Ventas diarias (con media móvil 7 días)",
                height=420,
                margin=dict(l=10, r=10, t=60, b=60),
                legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
                annotations=[
                    dict(
                        text=f"Fuente: {fuente} | Corte: {corte_str}",
                        x=0, y=-0.20, xref="paper", yref="paper",
                        showarrow=False,
                        align="left",
                        font=dict(size=12)
                    )
                ],
                plot_bgcolor='rgba(0,0,0,0)',
                paper_bgcolor='rgba(0,0,0,0)',
                hovermode="x unified"
            )

            st.plotly_chart(fig_line, use_container_width=True)
    # ------------------------------ Columna 2: Variaciones (%) ------------------------------
    with col2:
        # Datos base para variaciones
        # Agrupar por día y sumar ventas
        df_comparacion_periodos = (
            df_folios.loc[:, ["Fecha", "ventas_subtotal"]]
            .groupby("Fecha", as_index=False)["ventas_subtotal"].sum()
            .rename(columns={"ventas_subtotal": "ventas_dia"})
            .sort_values("Fecha")
            .reset_index(drop=True)
        )

        s = df_comparacion_periodos  # ya está ordenado; no copy

        if s.empty:
            st.markdown("### Variaciones de ventas")
            st.info("No hay datos suficientes para calcular variaciones.")
        else:
            # Continuidad diaria (rellena días faltantes con 0)
            full_idx = pd.date_range(s["Fecha"].min(), s["Fecha"].max(), freq="D")
            s = (
                s.set_index("Fecha")
                .reindex(full_idx)
                .fillna(0.0)
                .rename_axis("Fecha")
                .reset_index()
                .rename(columns={"index": "Fecha"})
            )

            # 1) Día vs Día anterior
            last_day = pd.to_datetime(s["Fecha"].max()).normalize()
            prev_day = last_day - pd.Timedelta(days=1)

            ventas_hoy  = float(s.loc[s["Fecha"] == last_day, "ventas_dia"].sum())
            ventas_ayer = float(s.loc[s["Fecha"] == prev_day, "ventas_dia"].sum()) if (s["Fecha"] == prev_day).any() else 0.0
            pct_dia = _safe_pct(ventas_hoy, ventas_ayer)

            rango_dia_actual   = _fmt_range(last_day)
            rango_dia_anterior = _fmt_range(prev_day) if (s["Fecha"] == prev_day).any() else "—"

            # 2) Semana vs Semana anterior (últimos 7 días vs previos 7)
            semana_actual_ini = last_day - pd.Timedelta(days=6)
            semana_actual_fin = last_day
            semana_prev_fin   = semana_actual_ini - pd.Timedelta(days=1)
            semana_prev_ini   = semana_prev_fin - pd.Timedelta(days=6)

            mask_sem_act  = (s["Fecha"] >= semana_actual_ini) & (s["Fecha"] <= semana_actual_fin)
            mask_sem_prev = (s["Fecha"] >= semana_prev_ini) & (s["Fecha"] <= semana_prev_fin)

            ventas_sem_act  = float(s.loc[mask_sem_act,  "ventas_dia"].sum())
            ventas_sem_prev = float(s.loc[mask_sem_prev, "ventas_dia"].sum())
            pct_semana = _safe_pct(ventas_sem_act, ventas_sem_prev)

            # 3) Mes vs Mes anterior (validación de mes completo)
            month_end = (last_day + MonthEnd(0)).normalize()
            is_full_month = (last_day == month_end)

            if is_full_month:
                # Mes actual completo vs anterior completo
                mes_act_ini = last_day.replace(day=1)
                mes_act_fin = last_day

                mes_prev_fin = mes_act_ini - pd.Timedelta(days=1)
                mes_prev_ini = mes_prev_fin.replace(day=1)

                rango_mes_act  = _fmt_range(mes_act_ini, mes_act_fin)
                rango_mes_prev = _fmt_range(mes_prev_ini, mes_prev_fin)

                mask_mes_act  = (s["Fecha"] >= mes_act_ini) & (s["Fecha"] <= mes_act_fin)
                mask_mes_prev = (s["Fecha"] >= mes_prev_ini) & (s["Fecha"] <= mes_prev_fin)

                ventas_mes_act  = float(s.loc[mask_mes_act,  "ventas_dia"].sum())
                ventas_mes_prev = float(s.loc[mask_mes_prev, "ventas_dia"].sum())
                pct_mes = _safe_pct(ventas_mes_act, ventas_mes_prev)

                show_partial_metric = False
            else:
                # Mes actual incompleto => comparar último mes completo vs su anterior (Julio vs Junio)
                mes_act_ini_real = last_day.replace(day=1)
                last_full_end = mes_act_ini_real - pd.Timedelta(days=1)  # fin del último mes completo
                last_full_ini = last_full_end.replace(day=1)

                prev_full_end = last_full_ini - pd.Timedelta(days=1)
                prev_full_ini = prev_full_end.replace(day=1)

                rango_mes_act  = _fmt_range(last_full_ini, last_full_end)   # último mes completo
                rango_mes_prev = _fmt_range(prev_full_ini, prev_full_end)   # mes completo anterior

                mask_mes_act  = (s["Fecha"] >= last_full_ini) & (s["Fecha"] <= last_full_end)
                mask_mes_prev = (s["Fecha"] >= prev_full_ini) & (s["Fecha"] <= prev_full_end)

                ventas_mes_act  = float(s.loc[mask_mes_act,  "ventas_dia"].sum())
                ventas_mes_prev = float(s.loc[mask_mes_prev, "ventas_dia"].sum())
                pct_mes = _safe_pct(ventas_mes_act, ventas_mes_prev)

                # 4) Periodo en curso del mes (1→k) vs mismo rango del mes anterior
                show_partial_metric = True
                k = last_day.day

                partial_act_ini = mes_act_ini_real
                partial_act_fin = last_day

                prev_month_end = mes_act_ini_real - pd.Timedelta(days=1)
                partial_prev_ini = prev_month_end.replace(day=1)
                partial_prev_fin = partial_prev_ini + pd.Timedelta(days=k-1)

                mask_par_act  = (s["Fecha"] >= partial_act_ini) & (s["Fecha"] <= partial_act_fin)
                mask_par_prev = (s["Fecha"] >= partial_prev_ini) & (s["Fecha"] <= partial_prev_fin)

                ventas_par_act  = float(s.loc[mask_par_act,  "ventas_dia"].sum())
                ventas_par_prev = float(s.loc[mask_par_prev, "ventas_dia"].sum())
                pct_parcial_mes = _safe_pct(ventas_par_act, ventas_par_prev)

                rango_parcial_act  = _fmt_range(partial_act_ini, partial_act_fin)
                rango_parcial_prev = _fmt_range(partial_prev_ini, partial_prev_fin)

            # Render de tarjetas (2 columnas × 2 filas)
            with st_card(""):
                row1_col1, row1_col2 = st.columns(2)
                row2_col1, row2_col2 = st.columns(2)

                with row1_col1:
                    _render_var_card(
                        titulo="Día vs Día anterior",
                        pct=pct_dia,
                        leyenda="Incremento de ventas respecto al día anterior",
                        rango_actual=rango_dia_anterior,
                        rango_anterior=rango_dia_actual 
                    )

                with row1_col2:
                    _render_var_card(
                        titulo="Semana vs Semana anterior",
                        pct=pct_semana,
                        leyenda="Últimos 7 días vs los 7 días previos",
                        rango_actual=_fmt_range(semana_prev_ini, semana_prev_fin),
                        rango_anterior=_fmt_range(semana_actual_ini, semana_actual_fin),
                    )

                with row2_col1:
                    _render_var_card(
                        titulo="Mes vs Mes anterior",
                        pct=pct_mes,
                        leyenda="Mes completo vs mes completo anterior",
                        rango_actual=rango_mes_prev,
                        rango_anterior= rango_mes_act,
                    )

                if 'show_partial_metric' in locals() and show_partial_metric:
                    with row2_col2:
                        _render_var_card(
                            titulo="Periodo en curso del mes",
                            pct=pct_parcial_mes,
                            leyenda="Del 1 al día actual vs mismo rango del mes anterior",
                            rango_actual=rango_parcial_prev,
                            rango_anterior=rango_parcial_act,
                        )


    # Analsisi de ventas_Sub total
    # -----------------------------------------
    # 0) Preparar base del periodo a graficar
    #    Usa cutoff_month_dt si quieres excluir mes incompleto (para YTD).
    # -----------------------------------------
    mask_plot = df_folios["Fecha"] >= ini_cur
    df_plot = df_folios.loc[mask_plot].copy()

    # ============================================================
    # VALIDACIÓN: ¿Tenemos al menos 1 mes COMPLETO?
    # ============================================================

    if df_plot.empty:
        meses_completos = []
    else:
        # Conteo real de días por mes en datos
        dias_por_mes = (
            df_plot
            .assign(mes=df_plot["Fecha"].dt.to_period("M"))
            .groupby("mes")["Fecha"]
            .nunique()
            .reset_index(name="dias_con_datos")
        )

        # Días calendario reales por mes
        dias_por_mes["dias_calendario"] = dias_por_mes["mes"].apply(
            lambda p: p.to_timestamp("M").day
        )

        # Mes completo = tiene todos los días
        meses_completos = dias_por_mes[
            dias_por_mes["dias_con_datos"] >= dias_por_mes["dias_calendario"]
        ]["mes"].tolist()

    if len(meses_completos) >= 1:

        # Tipos seguros
        df_plot["Fecha"] = pd.to_datetime(df_plot["Fecha"], errors="coerce")
        df_plot["ventas_subtotal"] = pd.to_numeric(df_plot["ventas_subtotal"], errors="coerce").fillna(0)

        # Definición de ingreso por folio (ajusta si luego agregas impuesto/descuento por folio)
        # ingreso_folio = ventas_subtotal (por ahora)
        df_plot["ingreso_folio"] = df_plot["ventas_subtotal"]

        # -----------------------------------------
        # 1) Serie mensual (df_monthly) para Treemap
        # -----------------------------------------
        df_plot["mes_period"] = df_plot["Fecha"].dt.to_period("M")

        df_monthly = (
            df_plot
            .groupby("mes_period", as_index=False)["ingreso_folio"]
            .sum()
            .rename(columns={"ingreso_folio": "ingreso"})
        )

        # Columnas auxiliares para tu lógica de labels
        df_monthly["mes_dt"] = df_monthly["mes_period"].dt.to_timestamp()
        df_monthly["mes"] = df_monthly["mes_dt"].dt.strftime("%Y-%m")

        # -----------------------------------------
        # 2) Serie diaria (ddf) para Heatmap
        # -----------------------------------------
        df_plot["fecha_dia"] = df_plot["Fecha"].dt.floor("D")

        ddf = (
            df_plot
            .groupby("fecha_dia", as_index=False)["ingreso_folio"]
            .sum()
            .rename(columns={"ingreso_folio": "ingreso"})
        ).sort_values("fecha_dia")


        # 3 COLUMNAS DE GRÁFICAS
        c1, c2 = st.columns(2)

        # ================================================================================================
        # C1) TREEMAP: Participación de ingreso por mes (% + $)
        # ================================================================================================
        with c1:
            # ============================================================
            # Treemap — Participación de ingreso por mes (multi-año si aplica)
            # ============================================================
            df_bar = df_monthly.copy()

            # Convertir a datetime y extraer año/mes
            dt_mes = pd.to_datetime(df_bar["mes_dt"], errors="coerce")
            df_bar["year"] = dt_mes.dt.year
            df_bar["mes_label"] = dt_mes.dt.strftime('%b %Y').str.capitalize()  # Ej: "Ene 2025"

            # Total general para participación
            total_ing = df_bar["ingreso"].sum()
            df_bar["participacion"] = np.where(total_ing > 0, df_bar["ingreso"] / total_ing, 0.0)

            # Label ejecutivo dentro del bloque (igual que el primero)
            df_bar["Label"] = df_bar.apply(
                lambda r: (
                    f"{r['mes_label']}<br>"
                    f"{fmt_money_abbrev(r['ingreso'])}<br>"
                    f"{r['participacion']*100:.1f}%"
                ),
                axis=1
            )

            # --- Treemap ---
            fig_tree = px.treemap(
                df_bar,
                path=[px.Constant("Todos los años"), "year", "mes_label"],  # Anillo central: "Todos los años"
                values="ingreso",
                color="participacion",
                color_continuous_scale="Blues",  # Consistente con el primero (o usa PICTON_BLUE si prefieres)
                hover_data={
                    "ingreso": ":,.0f",
                    "participacion": ":.1%"
                },
            )

            # Ajustes visuales clave (iguales al primero)
            fig_tree.update_traces(
                text=df_bar["Label"],
                textinfo="text",
                hovertemplate=(
                    "<b>%{label}</b><br>"
                    "Ingreso: $%{value:,.0f}<br>"
                    "Participación: %{color:.1%}"
                    "<extra></extra>"
                ),
            )

            # Footer: Fuente + Corte
            corte_str = f"{corte.day:02d}-{MES_MAP[corte.month]}-{corte.year}" if pd.notna(corte) else "—"

            fig_tree.update_layout(
                title="Participación de ingresos por mes",
                height=420,
                margin=dict(l=10, r=10, t=30, b=55),
                coloraxis_colorbar=dict(
                    title="Participación",
                    tickformat=".0%"
                ),
                annotations=[
                    dict(
                        text=f"Fuente: {fuente} | Corte: {corte_str}",
                        x=0, y=-0.18, xref="paper", yref="paper",
                        showarrow=False,
                        align="left",
                        font=dict(size=12),
                    )
                ]
            )

            # Aplicar tema Cabanna y mostrar
            st.plotly_chart(fig_tree, use_container_width=True)


        # ================================================================================================
        # C2) HEATMAP: Promedio de ingresos por día de la semana (por mes)
        # ================================================================================================
        with c2:
            if ddf.empty:
                st.info("No hay datos diarios para construir el heatmap.")
            else:
                # Completar continuidad diaria (días faltantes = 0)
                full_idx = pd.date_range(ddf["fecha_dia"].min(), ddf["fecha_dia"].max(), freq="D")
                ddf2 = (
                    ddf.set_index("fecha_dia")
                    .reindex(full_idx)
                    .fillna(0.0)
                    .rename_axis("fecha_dia")
                    .reset_index()
                )

                MESES_ES = ["Enero","Febrero","Marzo","Abril","mayo","Junio",
                            "Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"]
                DIAS_ES  = ["Lunes","Martes","Miércoles","Jueves","Viernes","Sábado","Domingo"]

                # Enriquecer: mes y día de semana
                ddf2["mes_period"] = ddf2["fecha_dia"].dt.to_period("M")
                ddf2["mes_label"]  = ddf2["mes_period"].apply(lambda p: f"{MESES_ES[p.month-1].capitalize() } {p.year}")
                ddf2["dow_idx"]    = ddf2["fecha_dia"].dt.dayofweek
                ddf2["dow_label"]  = ddf2["dow_idx"].map(dict(enumerate(DIAS_ES)))

                # Promedio de ingreso por mes × día de semana
                grp = (
                    ddf2.groupby(["mes_period", "mes_label", "dow_idx", "dow_label"], as_index=False)["ingreso"]
                    .mean()
                    .rename(columns={"ingreso": "promedio"})
                )

                grp = grp.sort_values(["mes_period", "dow_idx"])
                pivot = grp.pivot_table(index="mes_label", columns="dow_label", values="promedio", fill_value=0.0)
                pivot = pivot.reindex(columns=DIAS_ES)
                # Usar el orden único de mes_label que viene del groupby (ya está ordenado por mes_period)

                ordered_months = grp["mes_label"].unique()
                pivot = pivot.reindex(ordered_months)


                colorscale = [
                    [0.00, "#b8e3ff"],
                    [0.25, "#78cdff"],
                    [0.50, "#38b6ff"],
                    [0.75, "#007ace"],
                    [1.00, "#062b4b"],
                ]

                # ============================================================
                # Heatmap — Ingreso diario promedio por día de semana
                # ============================================================
    
                fig_heat = go.Figure(data=go.Heatmap(
                    z=pivot.values,
                    x=pivot.columns.tolist(),      # Días de la semana
                    y=pivot.index.tolist(),        # Meses (ordenados Ene → Dic)
                    colorscale="Blues",
                    colorbar=dict(title="Promedio $"),
                    text=np.vectorize(fmt_money_abbrev)(pivot.values),  # Valores abreviados dentro de celdas
                    texttemplate="%{text}",
                    textfont=dict(size=12, color="black"),
                    hovertemplate=(
                        "<b>%{y}</b><br>"          # Mes
                        "%{x}<br>"                 # Día de semana
                        "Promedio ingreso: <b>%{customdata}</b><extra></extra>"
                    ),
                    customdata=np.vectorize(fmt_money_abbrev)(pivot.values),  # Hover con valor abreviado
                ))

                # Layout ejecutivo (igual que el anterior)
                corte_str = f"{corte.day:02d}-{MES_MAP[corte.month]}-{corte.year}" if pd.notna(corte) else "—"

                fig_heat.update_layout(
                    title="Promedio de ingresos por día de la semana (por mes)",
                    height=420,
                    margin=dict(l=10, r=10, t=30, b=55),
                    yaxis=dict(
                        autorange="reversed"  # Enero arriba, Diciembre abajo
                    ),
                    xaxis=dict(
                        title=None,
                        showgrid=False
                    ),
                    yaxis_title=None,
                    annotations=[
                        dict(
                            text=f"Fuente: {fuente} | Corte: {corte_str}",
                            x=0, y=-0.18, xref="paper", yref="paper",
                            showarrow=False,
                            align="left",
                            font=dict(size=12)
                        )
                    ]
                )

                # Aplicar tema y mostrar
                st.plotly_chart(fig_heat, use_container_width=True)



    else:
        # ============================================================
        # ESTADO VACÍO ELEGANTE (UX PRO)
        # ============================================================
        st.info(
            "**Análisis mensual no disponible aún**\n\n"
            "Para construir este análisis se requiere al menos **un mes completo de información**.\n\n"
            "**Estado actual:**\n"
            f"- Días con datos: **{df_plot['Fecha'].nunique():,}**\n"
            f"- Meses completos disponibles: **0**\n\n"
            "Este panel se habilitará automáticamente cuando el mes concluya."
        )


    # ===============================================================================
    # Análisis de Clientes y Tickets (todas las sucursales)
    #    - KPIs: #tickets, ticket promedio, min, max
    #    - Histograma de ingreso por ticket
    #    - % de tickets por rangos de gasto
    # ===============================================================================

    st.markdown("---")
    st.markdown(f"## Análisis de Tickets")
    
    # 3) Crea la llave temporal mensual (timestamp al inicio de mes).
    df_folios["Mes"] = df_folios["Fecha"].dt.to_period("M").dt.to_timestamp()


        # selector de mes (último por defecto)
    # (opcional) para nombres en español según tu sistema
    # En Windows a veces es: 'Spanish_Mexico.1252'
    # Si falla, no pasa nada (abajo te dejo fallback)
    try:
        locale.setlocale(locale.LC_TIME, "es_MX")
    except:
        try:
            locale.setlocale(locale.LC_TIME, "Spanish_Mexico.1252")
        except:
            pass

    # 1) Filtrar al año seleccionado
    kpis_y = df_folios[df_folios["Mes"].dt.year == anio].copy()

    # 2) Labels bonitos
    # Si locale no aplica, usamos un diccionario fijo (fallback)

    kpis_y["MesLabel"] = kpis_y["Mes"].apply(lambda d: f"{MES_MAP[d.month]} {d.year}")

    # 3) Selectbox: muestra label, pero regresa la fila correcta
    labels = kpis_y["MesLabel"].unique().tolist()

    mes_label_sel = st.selectbox(
        "Mes",
        options=labels,
        index=len(labels)-1,
        label_visibility="collapsed",
        key=f"mes_kpi_{anio}"
    )

    row = kpis_y.loc[kpis_y["MesLabel"] == mes_label_sel].iloc[0]

    mes_label_map = dict(zip(kpis_y["Mes"], kpis_y["MesLabel"]))


    # Mes más reciente disponible (inicio de mes)
    mes_sel = row["Mes"]  # Timestamp del mes seleccionado (inicio de mes)

    df_tickets = build_df_tickets(df_folios)
    tm = ticket_metrics(df_tickets, mes_sel)
    # Mes anterior
    mes_prev = (mes_sel - pd.offsets.MonthBegin(1)).normalize().replace(day=1)
    mes_prev_label = f"{MES_MAP[mes_prev.month]} {mes_prev.year}"
    
    # k1, k2, k3, k4, k5 = st.columns(5)
    k1, k2, k4, k5 = st.columns(4)

    with k1:
        st.metric(
            f"Ticket promedio  {MES_MAP[mes_sel.month]} {mes_sel.year}",
            f"${tm['ticket_avg_mes']:,.2f}" if not pd.isna(tm["ticket_avg_mes"]) else "—",
            delta=fmt_delta_pct_or_abs(tm["ticket_avg_mes"], tm["ticket_avg_prev"], kind="currency"),
            help=(
                f"Ticket promedio mes anterior ({mes_prev_label}): "
                f"${tm['ticket_avg_prev']:,.2f}"
                if not pd.isna(tm["ticket_avg_prev"])
            else "Sin datos del mes anterior."
            )
        )

    with k2:
        st.metric(
            f"Tickets totales  {MES_MAP[mes_sel.month]} {mes_sel.year}",
            f"{tm['n_tickets_mes']:,}",
            delta=fmt_delta_pct_or_abs(tm["n_tickets_mes"], tm["n_tickets_prev"], kind="int"),
            help=(f"Mes anterior ({mes_prev_label}): {tm['n_tickets_prev']:,}"
                   if not pd.isna(tm["n_tickets_prev"])
                else "Sin datos del mes anterior."
                  )
        )


    with k4:
        st.metric(
            f"Ticket mínimo {MES_MAP[mes_sel.month]} {mes_sel.year}",
            f"${tm['ticket_min']:,.2f}" if not pd.isna(tm["ticket_min"]) else "—",
            help=f"Tickets con el mínimo este mes: {tm['n_ticket_min']:,}"
        )

    with k5:
        st.metric(
            f"Ticket máximo  {MES_MAP[mes_sel.month]} {mes_sel.year}",
            f"${tm['ticket_max']:,.2f}" if not pd.isna(tm["ticket_max"]) else "—",
            help=f"Fecha del ticket máximo: {tm['ticket_max_fecha']}"
        )

     # Base tickets del mes seleccionado
    df_tickets_mes = df_tickets[df_tickets["Mes"] == mes_sel].copy()

    # Ingreso por ticket por día (para gráfico diario)
    df_tickets_mes["Dia"] = df_tickets_mes["fecha_ticket"].dt.normalize()

    # ---------- Visuales ----------
    c1_1, c2_1 = st.columns(2)

    with c1_1:
        daily = (
            df_tickets_mes.groupby("Dia", as_index=False)
            .agg(ticket_promedio=("ingreso_ticket", "mean"), n_tickets=("ticket_id", "nunique"))
            .sort_values("Dia")
        )

        # Suavizado opcional (7 días) para quitar ruido diario
        daily["MA_7D"] = daily["ticket_promedio"].rolling(7, min_periods=1).mean()

        fig = go.Figure()

        # Serie real
        fig.add_trace(go.Scatter(
            x=daily["Dia"], y=daily["ticket_promedio"],
            mode="lines+markers",
            name="Ticket promedio diario",
            hovertemplate="%{x|%d-%b-%Y}<br><b>$%{y:,.0f}</b><extra></extra>"
        ))

        # Tendencia (MA 7 días)
        fig.add_trace(go.Scatter(
            x=daily["Dia"], y=daily["MA_7D"],
            mode="lines",
            name="Tendencia (MA 7D)",
            line=dict(width=2, dash="dot", color=PICTON_BLUE[5]),
            opacity=0.55,
            hovertemplate="MA 7D<br>%{x|%d-%b-%Y}<br><b>$%{y:,.0f}</b><extra></extra>"
        ))

        # Línea de promedio mensual (baseline)
        month_avg = float(df_tickets_mes["ingreso_ticket"].mean()) if len(df_tickets_mes) else np.nan
        if not np.isnan(month_avg):
            fig.add_hline(
                y=month_avg,
                line_dash="dot",
                opacity=0.4,
                annotation_text=f"Promedio mes: ${month_avg:,.0f}",
                annotation_position="top left"
            )

        # Highlights max/min diarios
        if len(daily):
            i_max = daily["ticket_promedio"].idxmax()
            i_min = daily["ticket_promedio"].idxmin()

            for idx, tag in [(i_max, "Máx"), (i_min, "Mín")]:
                fig.add_trace(go.Scatter(
                    x=[daily.loc[idx, "Dia"]],
                    y=[daily.loc[idx, "ticket_promedio"]],
                    mode="markers+text",
                    text=[f"{tag}: ${daily.loc[idx,'ticket_promedio']:,.0f}"],
                    textposition="top center",
                    showlegend=False,
                    hoverinfo="skip"
                ))

        # Eje X: días del mes (labels cortos)
        if len(daily):
            every = max(1, len(daily)//8)
            xvals = daily["Dia"].iloc[::every].tolist()
            xtext = [f"{d.day:02d}-{MES_MAP[d.month]}" for d in xvals]
            fig.update_xaxes(tickmode="array", tickvals=xvals, ticktext=xtext)

        # Eje Y abreviado ($K/$M)
        y = daily["ticket_promedio"].values if len(daily) else np.array([0])
        y_max = float(np.nanmax(y)) if len(y) else 0
        tickvals = np.unique(np.round(np.array([0, y_max*0.25, y_max*0.5, y_max*0.75, y_max]), 0))
        fig.update_yaxes(tickvals=tickvals.tolist(), ticktext=[fmt_money_abbrev(v) for v in tickvals])

        # Footer (fuente + corte)
        corte = df_tickets_mes["fecha_ticket"].max()
        corte_str = f"{corte.day:02d}-{MES_MAP[corte.month]}-{corte.year}" if pd.notna(corte) else "—"
        fig.update_layout(
            title="Ticket promedio por día (mes seleccionado)",
            height=420,
            margin=dict(l=10, r=10, t=40, b=60),
            legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
            annotations=[
                dict(
                    text=f"Fuente: Base de datos en servidores Cabanna | Corte: {corte_str}",
                    x=0, y=-0.20, xref="paper", yref="paper",
                    showarrow=False, align="left", font=dict(size=12),
                )
            ]
        )

        st.plotly_chart(fig, use_container_width=True)

    with c2_1:
        bins = [0, 200, 500, 1000, 2000, 5000, 10000, np.inf]
        labels = ["$0–199", "$200–499", "$500–999", "$1,000–1,999", "$2,000–4,999", "$5,000–9,999", "$10,000+"]

        df_b = df_tickets_mes.copy()
        df_b["rango"] = pd.cut(
            df_b["ingreso_ticket"],
            bins=bins,
            labels=labels,
            right=False,
            include_lowest=True
        )

        dist = (
            df_b.groupby("rango", as_index=False)
            .agg(
                n_tickets=("ticket_id", "nunique"),
                ticket_promedio=("ingreso_ticket", "mean")
            )
        )

        total = dist["n_tickets"].sum()
        dist["pct"] = np.where(total > 0, dist["n_tickets"] / total, 0)

        # Mantener orden de labels
        dist["rango"] = pd.Categorical(dist["rango"], categories=labels, ordered=True)
        dist = dist.sort_values("rango")

        # Gráfico barras (porcentaje)
        fig = go.Figure()
        fig.add_trace(go.Bar(
            x=dist["rango"].astype(str),
            y=dist["pct"],
            text=[f"{p*100:.1f}%" for p in dist["pct"]],
            textposition="outside",
            hovertemplate=(
                "<b>%{x}</b><br>"
                "Participación: %{y:.1%}<br>"
                "Tickets: %{customdata[0]:,}<br>"
                "Ticket promedio: $%{customdata[1]:,.0f}"
                "<extra></extra>"
            ),
            customdata=np.stack([dist["n_tickets"].fillna(0).astype(int), dist["ticket_promedio"].fillna(0)], axis=1)
        ))

        fig.update_yaxes(
            tickformat=".0%",
            range=[0, min(1.0, float(dist["pct"].max() * 1.25) if len(dist) else 1.0)]
        )

        fig.update_layout(
            title="Participación de tickets por rangos de gasto",
            height=420,
            margin=dict(l=10, r=10, t=40, b=60)
        )

        # Footer
        corte = df_tickets_mes["fecha_ticket"].max()
        corte_str = f"{corte.day:02d}-{MES_MAP[corte.month]}-{corte.year}" if pd.notna(corte) else "—"
        fig.add_annotation(
            text=f"Fuente: Base de datos en servidores Cabanna | Corte: {corte_str}",
            x=0, y=-0.20, xref="paper", yref="paper",
            showarrow=False, align="left", font=dict(size=12)
        )

        st.plotly_chart(fig, use_container_width=True)


    # =====================================================================================================================================================================
    # Comensales por día
    # =====================================================================================================================================================================
    df_folios["ventas_subtotal"] = pd.to_numeric(df_folios["ventas_subtotal"], errors="coerce").fillna(0.0)

    df_comensales = load_comensales(FILE_ID)

    if df_comensales is None or df_comensales.empty:
        st.error(
            "Datos de comensales no disponibles en este momento."
        )
        st.stop()
    else:
        # st.dataframe(df_comensales.head(3), use_container_width=True)
   
        st.markdown("---")
        st.markdown("### Ingreso por comensal")



        # 1) Rango real disponible (comensales)
        com_min = df_comensales["Fecha"].min()
        com_max = df_comensales["Fecha"].max()

        # Validar vs año seleccionado por el usuario
        anio_ini = pd.Timestamp(anio, 1, 1)
        anio_fin = pd.Timestamp(anio + 1, 1, 1) - pd.Timedelta(days=1)

        # Si el año seleccionado NO toca el rango de comensales -> mensaje y salir
        if (anio_fin < com_min) or (anio_ini > com_max):
            st.warning(
                f"Para el año **{anio}** no contamos con comensales por sucursal.\n\n"
                "Ajusta el filtro de año para ver esta sección."
            )
            
        else:
            # ==========================================================
            # 2) Rango por fuente (dentro del año seleccionado = 2025)
            # ==========================================================
            # Nota: aquí ya sabemos que el año toca el rango de comensales
            # y (por tu comentario) estamos trabajando solo 2025.

            # Fechas únicas por fuente
            fechas_com = pd.DatetimeIndex(df_comensales["Fecha"].dropna().unique()).sort_values()
            fechas_ven = pd.DatetimeIndex(df_folios["Fecha"].dropna().unique()).sort_values()

            # Rango real por fuente
            com_min_2025, com_max_2025 = fechas_com.min(), fechas_com.max()
            ven_min_2025, ven_max_2025 = fechas_ven.min(), fechas_ven.max()

            # (Opcional) forzar a año seleccionado (por seguridad)
            y_ini = pd.Timestamp(anio, 1, 1)
            y_fin = pd.Timestamp(anio + 1, 1, 1) - pd.Timedelta(days=1)

            fechas_com = fechas_com[(fechas_com >= y_ini) & (fechas_com <= y_fin)]
            fechas_ven = fechas_ven[(fechas_ven >= y_ini) & (fechas_ven <= y_fin)]

            if len(fechas_com) == 0:
                st.warning(f"No hay comensales dentro de {anio}.")
                st.stop()
            if len(fechas_ven) == 0:
                st.warning(f"No hay ventas dentro de {anio}.")
                st.stop()

            com_min_2025, com_max_2025 = fechas_com.min(), fechas_com.max()
            ven_min_2025, ven_max_2025 = fechas_ven.min(), fechas_ven.max()

            # ==========================================================
            # 3) Fechas que coinciden (intersección) = rango válido sección
            # ==========================================================
            fechas_ok = fechas_com.intersection(fechas_ven)

            if len(fechas_ok) == 0:
                st.warning(
                    f"No hay fechas coincidentes entre comensales y ventas en {anio}.\n\n"
                    f"Comensales: {com_min_2025:%d/%m/%Y} → {com_max_2025:%d/%m/%Y}\n"
                    f"Ventas: {ven_min_2025:%d/%m/%Y} → {ven_max_2025:%d/%m/%Y}"
                )
                st.stop()

            ok_min, ok_max = fechas_ok.min(), fechas_ok.max()

            st.caption(
                f"Esta sección solo considera las fechas del  **{ok_min:%d/%m/%Y} al {ok_max:%d/%m/%Y}** "
            )

            # ==========================================================
            # 4) Detectar faltantes por fuente (solo dentro del año)
            # ==========================================================
            # A) Ventas sí, comensales no
            faltan_comensales = fechas_ven.difference(fechas_com)

            # B) Comensales sí, ventas no
            faltan_ventas = fechas_com.difference(fechas_ven)
            
            mostrar_btn = (len(faltan_comensales) > 0) or (len(faltan_ventas) > 0)

            if len(faltan_comensales) > 0:
                ini_f = faltan_comensales.min()
                fin_f = faltan_comensales.max()
                st.warning(
                    f"⚠️ **Faltan el dato de número de comensales** para {len(faltan_comensales)} fechas donde sí hay ventas. "
                    f"Rango faltante: **{ini_f:%d/%m/%Y} → {fin_f:%d/%m/%Y}**"
                )

            if len(faltan_ventas) > 0:
                ini_f = faltan_ventas.min()
                fin_f = faltan_ventas.max()
                st.info(
                    f"ℹ️ **Faltan ventas** para {len(faltan_ventas)} fechas donde sí hay comensales. "
                    f"Rango faltante: **{ini_f:%d/%m/%Y} → {fin_f:%d/%m/%Y}**"
                )

            if mostrar_btn:
                cbtn1, cbtn2 = st.columns([3, 1])
                with cbtn2:
                    if st.button("🔄 Actualizar comensales", key="btn_refresh_comensales_global"):
                        st.cache_data.clear()
                        st.rerun()


            # Recortar el rango de trabajo (intersección año seleccionado ∩ rango comensales)
            rango_ini = max(com_min, anio_ini)
            rango_fin = min(com_max, anio_fin)

            # Filtra comensales al rango
            df_com_rango = df_comensales[(df_comensales["Fecha"] >= rango_ini) & (df_comensales["Fecha"] <= rango_fin)].copy()

            # Filtra ventas al mismo rango (para NO guardar/jalar cosas fuera del rango)
            df_folios_rango = df_folios[(df_folios["Fecha"] >= rango_ini) & (df_folios["Fecha"] <= rango_fin)].copy()

            # ---------------------------
            # 4) Agrupa ventas diarias por sucursal (en rango)
            # ---------------------------
            df_ventas_diarias_suc = (
                df_folios_rango
                .groupby(["Fecha", "NombreSucursal"], as_index=False)["ventas_subtotal"]
                .sum()
            )

            # ---------------------------
            # 5) Merge (solo dentro del rango)
            # ---------------------------
            df_merge = df_ventas_diarias_suc.merge(
                df_com_rango[["Fecha", "NombreSucursal", "comensales"]],
                on=["Fecha", "NombreSucursal"],
                how="inner"   # inner para quedarnos SOLO con lo que sí tiene comensales
            )


            df_merge['ingrteso_comensal'] = df_merge['ventas_subtotal']/df_merge['comensales']

            # ==========================================
            # KPIs con df_merge (sin consultas extra)
            # ==========================================

            df_merge["Fecha"] = pd.to_datetime(df_merge["Fecha"], errors="coerce").dt.normalize()
            df_merge["comensales"] = pd.to_numeric(df_merge["comensales"], errors="coerce").fillna(0)
            df_merge["ventas_subtotal"] = pd.to_numeric(df_merge["ventas_subtotal"], errors="coerce").fillna(0)

            # Evitar inf / div0
            df_merge["ingreso_comensal"] = df_merge["ventas_subtotal"] / df_merge["comensales"]
            df_merge.loc[df_merge["comensales"] <= 0, "ingreso_comensal"] = pd.NA

            # 1) Agregado diario (a nivel RED): sumas por día
            df_daily = (
                df_merge
                .groupby("Fecha", as_index=False)
                .agg(
                    ventas_dia=("ventas_subtotal", "sum"),
                    comensales_dia=("comensales", "sum")
                )
            )

            df_daily["ingreso_comensal_dia"] = df_daily["ventas_dia"] / df_daily["comensales_dia"]
            df_daily.loc[df_daily["comensales_dia"] <= 0, "ingreso_comensal_dia"] = pd.NA

            # 2) KPIs globales (rango completo)
            comensales_prom_dia = float(df_daily["comensales_dia"].mean()) if len(df_daily) else 0.0
            ingreso_prom_dia_por_com = float(df_daily["ingreso_comensal_dia"].mean()) if len(df_daily) else 0.0

            # 3) Mes en curso = MES CALENDARIO ACTUAL (aunque esté incompleto)
            #    (dentro del rango disponible de la sección)
            hoy = pd.Timestamp.today().normalize()
            mes_cur_ini = hoy.replace(day=1)
            mes_cur_fin = mes_cur_ini + pd.offsets.MonthBegin(1)

            # limitar al rango disponible (ok_min-ok_max) para que no busque fuera
            mes_cur_ini_eff = max(mes_cur_ini, ok_min)
            mes_cur_fin_eff = min(mes_cur_fin, ok_max + pd.Timedelta(days=1))

            df_mes_cur = df_daily[(df_daily["Fecha"] >= mes_cur_ini_eff) & (df_daily["Fecha"] < mes_cur_fin_eff)].copy()

            mes_cur_label = mes_cur_ini.strftime("%B %Y").capitalize()
            comensales_prom_dia_mes = float(df_mes_cur["comensales_dia"].mean()) if len(df_mes_cur) else 0.0
            ingreso_prom_dia_por_com_mes = float(df_mes_cur["ingreso_comensal_dia"].mean()) if len(df_mes_cur) else 0.0

            # 4) Mejor mes (por ingreso promedio diario por comensal)
            #    OJO: esto se calcula solo con meses que existan en df_daily (rango real disponible)
            if len(df_daily):
                df_daily["Mes"] = df_daily["Fecha"].dt.to_period("M").dt.to_timestamp()

                monthly = (
                    df_daily
                    .groupby("Mes", as_index=False)
                    .agg(
                        ingreso_com_prom=("ingreso_comensal_dia", "mean"),
                        comensales_prom=("comensales_dia", "mean"),
                        dias=("Fecha", "nunique"),
                    )
                    .sort_values("Mes")
                )

                # quitar meses sin ingreso válido
                monthly = monthly.dropna(subset=["ingreso_com_prom"])

                if len(monthly):
                    best_row = monthly.loc[monthly["ingreso_com_prom"].idxmax()]
                    best_mes = pd.to_datetime(best_row["Mes"])
                    best_mes_label = best_mes.strftime("%B %Y").capitalize()

                    # K6 y K7 (del mejor mes)
                    best_ingreso_com_dia = float(best_row["ingreso_com_prom"])
                    best_comensales_dia = float(best_row["comensales_prom"])
                    best_dias = int(best_row["dias"])
                else:
                    best_mes_label = "NA"
                    best_ingreso_com_dia = 0.0
                    best_comensales_dia = 0.0
                    best_dias = 0
            else:
                best_mes_label = "NA"
                best_ingreso_com_dia = 0.0
                best_comensales_dia = 0.0
                best_dias = 0

            # 5) Render KPIs (7 columnas)
            k1, k2, k3, k4 = st.columns(4)

            with k1:
                render_metric(
                    "Ingreso/comensal (día)",
                    fmt_currency(ingreso_prom_dia_por_com) if ingreso_prom_dia_por_com == ingreso_prom_dia_por_com else "NA",
                    help_text="Promedio diario de (Ingreso del día / Comensales del día) para el rango aplicado."
                )

            with k2:
                render_metric(
                    "Comensales prom./día",
                    f"{comensales_prom_dia:,.0f}",
                    help_text="Promedio diario de comensales (suma red por día) para el rango aplicado."
                )

            with k3:
                render_metric(
                    f"Ingreso/comensal — {mes_cur_label}",
                    fmt_currency(ingreso_prom_dia_por_com_mes) if ingreso_prom_dia_por_com_mes == ingreso_prom_dia_por_com_mes else "NA",
                    help_text="Mes calendario actual"
                )

            with k4:
                render_metric(
                    f"Comensales/día — {mes_cur_label}",
                    f"{comensales_prom_dia_mes:,.0f}",
                    help_text="Mes calendario actual"
                )

            k5, k6, k7 = st.columns(3)

            with k5:
                render_metric(
                    "Mejor mes (Ingreso/comensal)",
                    f"{best_mes_label}",
                    help_text="Mes del año con mayor ingreso promedio diario por comensal."
                )


            with k6:
                render_metric(
                    f"Ingreso/comensal — {best_mes_label}",
                    fmt_currency(best_ingreso_com_dia) if best_mes_label != "NA" else "NA",
                    help_text="Promedio diario de ingreso por comensal dentro del mejor mes."
                )

            with k7:
                render_metric(
                    f"Comensales/día — {best_mes_label}",
                    f"{best_comensales_dia:,.0f}" if best_mes_label != "NA" else "NA",
                    help_text=f"Promedio diario de comensales dentro del mejor mes. Días con datos: {best_dias}."
                )

            # -----------------------------------------
            # Helper: formato compacto (730k, 1.9M, etc.)
            # -----------------------------------------
            def _fmt_short(x):
                try:
                    if pd.isna(x):
                        return ""
                    x = float(x)
                    ax = abs(x)
                    if ax >= 1_000_000_000:
                        return f"{x/1_000_000_000:.1f}B"
                    if ax >= 1_000_000:
                        return f"{x/1_000_000:.1f}M"
                    if ax >= 1_000:
                        return f"{x/1_000:.1f}k"
                    return f"{x:,.0f}"
                except Exception:
                    return ""
            
            # -----------------------------------------
            # 0) Normalizaciones mínimas
            # -----------------------------------------
            df_merge["Fecha"] = pd.to_datetime(df_merge["Fecha"], errors="coerce").dt.normalize()
            df_merge["NombreSucursal"] = (
                df_merge["NombreSucursal"].astype(str)
                .str.replace(r"\s+", " ", regex=True)
                .str.strip()
                .str.upper()
            )
            df_merge["comensales"] = pd.to_numeric(df_merge["comensales"], errors="coerce").fillna(0)
            df_merge["ventas_subtotal"] = pd.to_numeric(df_merge["ventas_subtotal"], errors="coerce").fillna(0)


            if df_merge.empty:
                 meses_completos_c = []
            else:
                # Conteo real de días por mes en datos
                dias_por_mes = (
                    df_merge
                    .assign(mes=df_merge["Fecha"].dt.to_period("M"))
                    .groupby("mes")["Fecha"]
                    .nunique()
                    .reset_index(name="dias_con_datos")
                )

                # Días calendario reales por mes
                dias_por_mes["dias_calendario"] = dias_por_mes["mes"].apply(
                    lambda p: p.to_timestamp("M").day
                )

                # Mes completo = tiene todos los días
                meses_completos_c = dias_por_mes[
                    dias_por_mes["dias_con_datos"] >= dias_por_mes["dias_calendario"]
                ]["mes"].tolist()

            if len(meses_completos_c) >= 1:
                # -----------------------------------------
                # 1) Selector de sucursal (incluye "TODAS")
                # -----------------------------------------
                sucs = sorted(df_merge["NombreSucursal"].dropna().unique().tolist())
                opciones = ["TODAS"] + sucs

                col_sel1, col_sel2 = st.columns([2, 6], vertical_alignment="bottom")
                with col_sel1:
                    suc_sel = st.selectbox("Sucursal", options=opciones, index=0)
                with col_sel2:
                    st.caption("Selecciona una sucursal o 'TODAS' para ver la red completa.")

                df_base = df_merge.copy()
                if suc_sel != "TODAS":
                    df_base = df_base[df_base["NombreSucursal"] == suc_sel].copy()

                if df_base.empty:
                    st.warning("No hay datos para la selección actual.")
                    st.stop()

                # -----------------------------------------
                # 2) Agregar a nivel día (por la selección)
                # -----------------------------------------
                df_daily = (
                    df_base
                    .groupby("Fecha", as_index=False)
                    .agg(
                        ventas_dia=("ventas_subtotal", "sum"),
                        comensales_dia=("comensales", "sum")
                    )
                )

                df_daily["ingreso_por_comensal"] = df_daily["ventas_dia"] / df_daily["comensales_dia"]
                df_daily.loc[df_daily["comensales_dia"] <= 0, "ingreso_por_comensal"] = np.nan



                df_daily["mes_period"] = df_daily["Fecha"].dt.to_period("M")
                df_daily["mes_label"]  = df_daily["mes_period"].apply(lambda p: f"{MESES_ES[p.month-1]} {p.year}")
                df_daily["mes_sort"]   = df_daily["mes_period"].astype(str)

                df_daily["dow_idx"]    = df_daily["Fecha"].dt.dayofweek
                df_daily["dow_label"]  = df_daily["dow_idx"].map(dict(enumerate(DIAS_ES)))

                # -----------------------------------------
                # 4) Pivot ingreso promedio por comensal (mes x dow)
                # -----------------------------------------
                grp_ing = (
                    df_daily
                    .groupby(["mes_period", "mes_sort", "mes_label", "dow_idx", "dow_label"], as_index=False)["ingreso_por_comensal"]
                    .mean()
                    .rename(columns={"ingreso_por_comensal": "valor"})
                )

                pivot_ing = (
                    grp_ing
                    .sort_values(["mes_sort", "dow_idx"])
                    .pivot_table(index=["mes_sort","mes_label"], columns="dow_label", values="valor", fill_value=np.nan)
                    .reindex(columns=DIAS_ES)
                )

                # -----------------------------------------
                # 5) Pivot comensales promedio por día (mes x dow)
                # -----------------------------------------
                grp_com = (
                    df_daily
                    .groupby(["mes_period", "mes_sort", "mes_label", "dow_idx", "dow_label"], as_index=False)["comensales_dia"]
                    .mean()
                    .rename(columns={"comensales_dia": "valor"})
                )

                pivot_com = (
                    grp_com
                    .sort_values(["mes_sort", "dow_idx"])
                    .pivot_table(index=["mes_sort","mes_label"], columns="dow_label", values="valor", fill_value=np.nan)
                    .reindex(columns=DIAS_ES)
                )

                # -----------------------------------------
                # 6) Render 2 heatmaps (2 columnas)
                # -----------------------------------------
                c_comsnales_1, c_comsnales_2 = st.columns(2)

                # Preparar orden de meses correcto (Enero arriba)
                ordered_mes_labels = grp_ing["mes_label"].unique()  # ya ordenado por mes_sort
                # Si usas multiindex, extrae solo mes_label
                if isinstance(pivot_ing.index, pd.MultiIndex):
                    ordered_mes_labels = pivot_ing.index.get_level_values("mes_label").unique()

        
                # Footer igual que tu heatmap original
                corte_str = f"{corte.day:02d}-{MES_MAP[corte.month]}-{corte.year}" if pd.notna(corte) else "—"

                # ============ HEATMAP 1: $ por comensal ============
                with c_comsnales_1:

                    if pivot_ing.empty:
                        st.info("No hay suficientes datos para el heatmap de ingreso por comensal.")
                    else:
                        z = pivot_ing.values
                        rows = pivot_ing.index.get_level_values("mes_label").tolist()
                        cols = pivot_ing.columns.tolist()

                        # Texto interno estilo ejecutivo (con $ y abreviado opcional)
                        text =  np.vectorize(fmt_money_abbrev)(pivot_ing.values)

                        fig_heat_1 = go.Figure(
                            data=go.Heatmap(
                                z=z,
                                x=cols,
                                y=rows,
                                colorscale=colorscale,                 # <- tu escala exacta
                                colorbar=dict(title="$ / comensal"),
                                text=text,
                                texttemplate="%{text}",
                                textfont=dict(size=12, color="black"),
                                hovertemplate=(
                                    "<b>%{y}</b><br>"
                                    "%{x}<br>"
                                    "$ por comensal: <b>$%{z:,.2f}</b>"
                                    "<extra></extra>"
                                ),
                                showscale=True,
                            )
                        )

                        fig_heat_1.update_layout(
                            title=f"Ingreso promedio por comensal (día) — {suc_sel}",
                            height=420,
                            margin=dict(l=10, r=10, t=30, b=55),
                            yaxis=dict(autorange="reversed"),
                            xaxis=dict(title=None, showgrid=False),
                            yaxis_title=None,
                            annotations=[
                                dict(
                                    text=f"Fuente: {fuente} | Corte: {corte_str}",
                                    x=0, y=-0.18, xref="paper", yref="paper",
                                    showarrow=False,
                                    align="left",
                                    font=dict(size=12)
                                )
                            ],
                        )

                        fig_heat_1.update_xaxes(showgrid=False, tickfont=dict(color=COLORS.get("white", "#ddd")))
                        fig_heat_1.update_yaxes(showgrid=False, tickfont=dict(color=COLORS.get("white", "#ddd")))

                        st.plotly_chart(fig_heat_1, use_container_width=True)

                        st.info(
                            "Cómo leer: cada celda muestra el **promedio diario de $ por comensal** "
                            "para ese **mes** y **día de la semana**. "
                            "Más oscuro = mayor ingreso por comensal."
                        )

                # ============ HEATMAP 2: Comensales promedio ============
                with c_comsnales_2:

                    if pivot_com.empty:
                        st.info("No hay suficientes datos para el heatmap de comensales.")
                    else:
                        z = pivot_com.values
                        rows = pivot_com.index.get_level_values("mes_label").tolist()
                        cols = pivot_com.columns.tolist()

                        # Texto interno (sin $)
                        text = np.vectorize(_fmt_short)(z)

                        fig_heat_2 = go.Figure(
                            data=go.Heatmap(
                                z=z,
                                x=cols,
                                y=rows,
                                colorscale=colorscale,
                                colorbar=dict(title="Comensales"),
                                text=text,
                                texttemplate="%{text}",
                                textfont=dict(size=12, color="black"),
                                hovertemplate=(
                                    "<b>%{y}</b><br>"
                                    "%{x}<br>"
                                    "Comensales promedio: <b>%{z:,.0f}</b>"
                                    "<extra></extra>"
                                ),
                                showscale=True,
                            )
                        )

                        fig_heat_2.update_layout(
                            title= f"Comensales promedio por día (por mes)  — {suc_sel}",
                            height=420,
                            margin=dict(l=10, r=10, t=30, b=55),
                            yaxis=dict(autorange="reversed"),
                            xaxis=dict(title=None, showgrid=False),
                            yaxis_title=None,
                            annotations=[
                                dict(
                                    text=f"Fuente: {fuente} | Corte: {corte_str}",
                                    x=0, y=-0.18, xref="paper", yref="paper",
                                    showarrow=False,
                                    align="left",
                                    font=dict(size=12)
                                )
                            ],
                        )

                        fig_heat_2.update_xaxes(showgrid=False, tickfont=dict(color=COLORS.get("white", "#ddd")))

                        st.plotly_chart(fig_heat_2, use_container_width=True)

                        st.info(
                            "Cómo leer: cada celda muestra el **promedio diario de comensales** "
                            "para ese **mes** y **día de la semana**. "
                            "Más oscuro = más comensales."
                        )

                # ==========================================================
                # 0) Base diaria por sucursal (UNA sola vez)
                # ==========================================================
                df_daily = (
                    df_merge
                    .groupby(["Fecha", "NombreSucursal"], as_index=False)[["ventas_subtotal", "comensales"]]
                    .sum()
                )

                df_daily["Fecha"] = pd.to_datetime(df_daily["Fecha"], errors="coerce").dt.normalize()
                df_daily["ventas_subtotal"] = pd.to_numeric(df_daily["ventas_subtotal"], errors="coerce").fillna(0.0)
                df_daily["comensales"] = pd.to_numeric(df_daily["comensales"], errors="coerce").fillna(0.0)

                df_daily["mes_key"] = df_daily["Fecha"].dt.to_period("M").astype(str)   # '2025-12'
                df_daily["anio"]    = df_daily["Fecha"].dt.year
                df_daily["mes"]     = df_daily["Fecha"].dt.month
                df_daily["mes_label"] = df_daily.apply(lambda r: f"{MES_MAP[r['mes']]} {r['anio']}", axis=1)

                meses_cat = (
                    df_daily[["mes_key", "mes_label"]]
                    .drop_duplicates()
                    .sort_values("mes_key")
                )
                MAP_MESES = dict(zip(meses_cat["mes_label"], meses_cat["mes_key"]))
                opciones_ui = ["Todo el periodo"] + meses_cat["mes_label"].tolist()

                filtro_ui = st.selectbox(
                    "Periodo (aplica a ambas gráficas)",
                    options=opciones_ui,
                    index=0,
                    key="filtro_periodo_comensales"
                )

                if filtro_ui == "Todo el periodo":
                    df_daily_f = df_daily.copy()
                    filtro_txt = "Todo el periodo"
                else:
                    mes_key = MAP_MESES[filtro_ui]
                    df_daily_f = df_daily[df_daily["mes_key"] == mes_key].copy()
                    filtro_txt = filtro_ui

                if df_daily_f.empty:
                    st.warning("No hay datos para el periodo seleccionado.")
                    st.stop()

                # ==========================================================
                # 2) Agregación ÚNICA por sucursal (base para ambas gráficas)
                # ==========================================================
                df_suc = (
                    df_daily_f
                    .groupby("NombreSucursal", as_index=False)
                    .agg(
                        ingreso_total=("ventas_subtotal", "sum"),
                        comensales_total=("comensales", "sum"),
                        ingreso_prom_dia=("ventas_subtotal", "mean"),
                        comensales_prom_dia=("comensales", "mean"),
                        dias=("Fecha", "nunique")
                    )
                )

                # Eficiencia (del periodo)
                df_suc["eficiencia"] = np.where(
                    df_suc["comensales_total"] > 0,
                    df_suc["ingreso_total"] / df_suc["comensales_total"],
                    np.nan
                )

                # Para barras: $/comensal del periodo (no promedio diario)
                df_suc["ingreso_x_comensal"] = df_suc["eficiencia"]

                df_suc = df_suc.replace([np.inf, -np.inf], np.nan)
                df_suc["ingreso_x_comensal"] = pd.to_numeric(df_suc["ingreso_x_comensal"], errors="coerce")

                # Orden para barras (menor → mayor)
                df_bar = df_suc.sort_values("ingreso_x_comensal", ascending=True).copy()

                # ==========================================================
                # 3) Dos gráficas: Barras + Scatter (bubble)
                # ==========================================================
                c_comsnales_3, c_comsnales_4 = st.columns(2)

                # ---------------------------
                # c_comensales_3 — Barras
                # ---------------------------
                with c_comsnales_3:
                    with st_card(f"$ promedio por comensal — por sucursal ({filtro_txt})"):
                        if df_bar.empty or df_bar["ingreso_x_comensal"].dropna().empty:
                            st.info("No hay datos suficientes para calcular $/comensal.")
                        else:
                            fig_bar = px.bar(
                                df_bar,
                                x="ingreso_x_comensal",
                                y="NombreSucursal",
                                orientation="h",
                                text=df_bar["ingreso_x_comensal"].map(lambda v: "" if pd.isna(v) else f"${v:,.0f}")
                            )

                            fig_bar.update_traces(
                                textposition="inside",
                                insidetextanchor="middle",
                                textfont=dict(color="white"),
                                marker=dict(color=PICTON_BLUE[6]),
                                hovertemplate=(
                                    "Sucursal: %{y}<br>"
                                    "$/comensal (periodo): $%{x:,.2f}<br>"
                                    "Ingreso total: $%{customdata[0]:,.0f}<br>"
                                    "Comensales total: %{customdata[1]:,.0f}<br>"
                                    "Días: %{customdata[2]:,}<extra></extra>"
                                ),
                                customdata=df_bar[["ingreso_total", "comensales_total", "dias"]].values
                            )

                            fig_bar.update_layout(
                                margin=dict(l=10, r=10, t=10, b=10),
                                xaxis=dict(showgrid=False, title="Ingreso por comensal ($)", color=COLORS["white"]),
                                yaxis=dict(showgrid=False, title=None, color=COLORS["white"]),
                                height=520,
                                showlegend=False
                            )

                            st.plotly_chart(fig_bar, use_container_width=True)

                        st.info(
                            "Cómo leer: cada barra es una sucursal. "
                            "La longitud representa el **$ promedio por comensal del periodo seleccionado** "
                            "(Ingreso total / Comensales totales)."
                        )

                # ---------------------------
                # c_comensales_4 — Scatter (bubble)
                # ---------------------------
                with c_comsnales_4:
                    with st_card(f"Comensales vs Ingreso — por sucursal ({filtro_txt})"):
                        df_sc = df_suc.copy()

                        # Si quieres evitar ruido por sucursales con 0 comensales:
                        df_sc = df_sc[df_sc["comensales_total"] > 0].copy()

                        if df_sc.empty:
                            st.info("No hay datos suficientes para construir el scatter.")
                        else:
                            fig_sc = px.scatter(
                                df_sc,
                                x="comensales_prom_dia",
                                y="ingreso_prom_dia",
                                size="ingreso_total",            # tamaño = peso económico del periodo
                                color="eficiencia",              # color = $/comensal (más alto = mejor)
                                color_continuous_scale=colorscale if "colorscale" in globals() else PICTON_BLUE,
                                hover_name="NombreSucursal",
                                labels={
                                    "comensales_prom_dia": "Comensales promedio por día",
                                    "ingreso_prom_dia": "Ingreso promedio por día",
                                    "eficiencia": "Eficiencia ($/comensal)"
                                },
                                hover_data={
                                    "ingreso_total": ":,.0f",
                                    "comensales_total": ":,.0f",
                                    "eficiencia": ":,.2f",
                                    "dias": True,
                                    "comensales_prom_dia": ":,.0f",
                                    "ingreso_prom_dia": ":,.0f",
                                }
                            )

                            fig_sc.update_traces(
                                text=df_sc["NombreSucursal"],
                                textposition="top center",
                                marker=dict(line=dict(width=0)),
                                hovertemplate=(
                                    "<b>%{hovertext}</b><br><br>"
                                    "Comensales prom/día: %{x:,.0f}<br>"
                                    "Ingreso prom/día: $%{y:,.0f}<br>"
                                    "Eficiencia ($/comensal): $%{marker.color:,.2f}<br>"
                                    "Ingreso total (tamaño): $%{marker.size:,.0f}"
                                    "<extra></extra>"
                                )
                            )

                            fig_sc.update_layout(
                                margin=dict(l=10, r=10, t=10, b=10),
                                height=520,
                                xaxis=dict(showgrid=False, color=COLORS["white"]),
                                yaxis=dict(showgrid=False, color=COLORS["white"]),
                                coloraxis_colorbar=dict(title="$/comensal")
                            )

                            st.plotly_chart(fig_sc, use_container_width=True)

                        st.info(
                            "Cómo leer: cada punto es una sucursal. "
                            "**Más a la derecha** = más comensales promedio por día. "
                            "**Más arriba** = más ingreso promedio por día. "
                            "**Tamaño** = ingreso total del periodo (sucursales más grandes venden más). "
                            "**Color** = eficiencia ($ por comensal): tonos más altos indican mejor ingreso por comensal."
                        )
            else:
                # ============================================================
                # ESTADO VACÍO ELEGANTE (UX PRO)
                # ============================================================
                st.info(
                    "**Análisis mensual no disponible aún**\n\n"
                    "Para construir este análisis se requiere al menos **un mes completo de información**.\n\n"
                    "**Estado actual:**\n"
                    f"- Días con datos: **{df_merge['Fecha'].nunique():,}**\n"
                    f"- Meses completos disponibles: **0**\n\n"
                    "Este panel se habilitará automáticamente cuando el mes concluya."
                )




    # -----------------------------
    # 1) Agregación por sucursal (base del mapa)
    # -----------------------------
    # --- Base series (evita accesos repetidos) ---
    f = df_folios["Fecha"]
    mask_cur = (f >= ini_cur) & (f < fin_cur)

    # 1) Agregación por sucursal (DF pequeño)
    df_suc_tot = (
        df_folios.loc[mask_cur, ["NombreSucursal", "ventas_subtotal"]]
        .groupby("NombreSucursal", as_index=False, sort=False)["ventas_subtotal"]
        .sum()
    )

    if df_suc_tot.empty:
        prom_red = 0.0
        df_suc_tot["pct_vs_prom"] = 0.0
    else:
        prom_red = float(df_suc_tot["ventas_subtotal"].mean())
        if prom_red > 0:
            df_suc_tot["pct_vs_prom"] = (df_suc_tot["ventas_subtotal"] - prom_red) / prom_red
        else:
            df_suc_tot["pct_vs_prom"] = 0.0

    # -----------------------------
    # 2) Normalizar nombre para hacer match robusto
    #    (evita que "Culiacán", "CULIACAN ", "Culiacan" fallen)
    # -----------------------------
    def _norm_key(x: str) -> str:
        if x is None:
            return ""
        s = str(x).strip().upper()
        # remover acentos de forma general
        s = "".join(c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c))
        return s

    # df_suc_tot es pequeño -> aplicar aquí no duele
    df_suc_tot["sucursal_key"] = df_suc_tot["NombreSucursal"].map(_norm_key)

    # UI
    st.markdown("---")
    st.markdown("## Análisis de sucursal")

    #c1_1, c2_1, c3_1 = st.columns(3)
    c2_1, c3_1 = st.columns(2)

    # ------------------------------ c2: Barras por sucursal (ordenado) ------------------------------
    with c2_1:
        with st_card(f"Ingresos por sucursal en el año en {mes_sel.year}"):
            if df_suc_tot.empty:
                st.info("No hay datos de ingresos por sucursal.")
            else:
                df_bars = df_suc_tot.sort_values("ventas_subtotal", ascending=True, kind="mergesort")

                # Pre-calcular textos (list comprehension es rápido)
                text_monto = [f"${v:,.0f}" for v in df_bars["ventas_subtotal"].to_numpy()]
                pct_vals = df_bars["pct_vs_prom"].to_numpy()
                ventas_vals = df_bars["ventas_subtotal"].to_numpy()

                fig_suc = px.bar(
                    df_bars,
                    x="ventas_subtotal", y="NombreSucursal",
                    orientation="h",
                    text=text_monto
                )

                # Customdata sin .assign/.values (menos overhead)
                customdata = np.column_stack([
                    ventas_vals,
                    np.full_like(ventas_vals, prom_red, dtype=float),
                    pct_vals
                ])

                fig_suc.update_traces(
                    textposition="inside",
                    insidetextanchor="middle",
                    textfont=dict(color="white"),
                    marker=dict(color=PICTON_BLUE[6]),
                    hovertemplate=(
                        "Sucursal: %{y}<br>"
                        "Ingreso: $%{customdata[0]:,.2f}<br>"
                        "Promedio red: $%{customdata[1]:,.2f}<br>"
                        "% vs promedio: %{customdata[2]:+.2%}<extra></extra>"
                    ),
                    customdata=customdata
                )

                fig_suc.add_vline(x=prom_red, line_width=2, line_dash="dash", line_color=PICTON_BLUE[4])

                # Anotaciones sin iterrows (zip + listas)
                annotations = [
                    dict(
                        x=float(x),
                        y=y,
                        xanchor="left",
                        yanchor="middle",
                        xshift=6,
                        showarrow=False,
                        text=f"{p:+.1%}",
                        font=dict(color=("#22c55e" if p >= 0 else "#ef4444"), size=12),
                    )
                    for x, y, p in zip(ventas_vals, df_bars["NombreSucursal"].tolist(), pct_vals)
                ]

                fig_suc.update_layout(
                    annotations=annotations,
                    margin=dict(l=10, r=10, t=10, b=10),
                    xaxis=dict(showgrid=False, color=COLORS["white"], title=None),
                    yaxis=dict(showgrid=False, color=COLORS["white"], title=None),
                    height=520,
                )

                st.plotly_chart(fig_suc, use_container_width=True)


    # ------------------------------ c3: Top sucursales vs promedio (tabla/insights) ------------------------------
    with c3_1:
        st.markdown("**Semáforo de desempeño por sucursal (vs promedio de la red)**")
        if df_suc_tot.empty:
            st.info("No hay datos mensuales por sucursal.")
        else:
            df_sem = df_suc_tot  # no copy

            p = df_sem["pct_vs_prom"].to_numpy()
            status = np.where(p >= 0.10, "Verde", np.where(p <= -0.10, "Rojo", "Amarillo"))
            df_sem = df_sem.assign(status=status)

            # Orden: Verde arriba, Amarillo medio, Rojo abajo (o como tú quieras)
            order = {"Verde": 0, "Amarillo": 1, "Rojo": 2}
            df_sem["_ord"] = df_sem["status"].map(order)
            df_sem = df_sem.sort_values(["_ord", "ventas_subtotal"], ascending=[True, False]).drop(columns="_ord")

            color_map = {"Verde":"#22c55e","Amarillo":"#eab308","Rojo":"#ef4444"}

            cols = st.columns(3)
            # itertuples es MUCHO más rápido que iterrows
            for i, row in enumerate(df_sem.itertuples(index=False)):
                c = cols[i % 3]
                dot = color_map.get(row.status, "#cfcfcf")

                with c:
                    st.markdown(
                        f"""
                        <div style="background:#0b0b0b;border:1px solid rgba(212,175,55,.18);
                                    border-radius:12px;padding:10px 12px;margin-bottom:10px;">
                        <div style="display:flex;align-items:center;gap:8px;">
                            <div style="width:10px;height:10px;border-radius:50%;background:{dot};"></div>
                            <div style="
                                font-weight:500;font-size:0.8rem;line-height:1.2;
                                white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;
                            " title="{row.NombreSucursal}">
                                {row.NombreSucursal}
                            </div>
                        </div>
                        <div style="color:#cfcfcf;font-size:.9rem;margin-top:6px;">
                            % vs prom: {row.pct_vs_prom:+.1%}
                        </div>
                        </div>
                        """,
                        unsafe_allow_html=True
                    )


    # ===============================================================================
    # KPIs del ÚLTIMO MES COMPLETO (por sucursal) — usando df_folios
    # ===============================================================================

    st.markdown("### KPIs del último mes completo (por sucursal)")
       
    # ---------------------------------------------------------------------------
    # BLOQUE 1: VALIDACIÓN TEMPRANA (Fail Fast)
    # ---------------------------------------------------------------------------
    if df_folios.empty:
        st.info("No hay datos de folios para calcular KPIs.")

    # ---------------------------------------------------------------------------
    # BLOQUE 2: LÓGICA DE CALENDARIO (Preparar los datos para el selector)
    # ---------------------------------------------------------------------------

    # 1. Detectar la fecha "Techo" (la última venta registrada en todo el dataset)
    last_day_data = pd.to_datetime(df_folios["Fecha"].max())

    # 2. Agrupar folios existentes por mes (Primer día del mes)
    meses_disponibles = (
        df_folios
        .assign(mes=df_folios["Fecha"].dt.to_period("M").dt.to_timestamp())
        .groupby("mes")
        .size()
        .reset_index(name="n")
    )

    # 3. Calcular el fin de mes de cada periodo disponible
    meses_disponibles["month_end"] = (
        meses_disponibles["mes"] + pd.offsets.MonthEnd(0)
    ).dt.normalize()

    # 4. FILTRO MAESTRO: ¿Qué es un "Mes Completo"?
    # Regla: Un mes se considera completo si su último día es menor o igual 
    # a la última fecha registrada en los datos.
    # (Ej: Si hoy es 7 Enero, Enero NO es completo, pero Diciembre SI).
    meses_completos = meses_disponibles[
        meses_disponibles["month_end"] <= last_day_data.normalize()
    ].copy()

    # Ordenar cronológicamente (importante para el selectbox)
    meses_completos = meses_completos.sort_values("mes", ascending=True)

    if meses_completos.empty:
        st.info("Aún no hay meses cerrados completos disponibles para análisis.")


    meses_completos_y = meses_completos[
        meses_completos["mes"].dt.year == anio
    ].copy()

    # ---------------------------------------------------------------------------
    # BLOQUE 3: DETERMINAR EL DEFAULT INTELIGENTE
    # ---------------------------------------------------------------------------
    
    # (LEYENDA) Si el mes actual NO está cerrado, explicarlo al usuario
    # Ej: estamos a 8 de enero 2026 -> enero no está completo, solo se analiza diciembre 2025
    month_end_data = (last_day_data + pd.offsets.MonthEnd(0)).normalize()
    if last_day_data.normalize() < month_end_data:
        st.caption(
            f"**Nota:** El mes actual (**{MES_MAP[last_day_data.month]} {last_day_data.year}**) "
            f"aún no está cerrado (último dato: **{last_day_data:%d/%m/%Y}**). "
            "Para mantener comparaciones consistentes, esta sección usa **solo meses completos**."
        )

        # Por defecto: último mes completo disponible (pero SOLO mostramos 1 opción)
        lista_opciones = (
            meses_completos["mes"]
            .sort_values(ascending=False)
            .head(1)                 # <-- SOLO el último mes completo
            .tolist()
        )

        default_idx = 0  # el más reciente (y único)

        def fmt_mes_abrev(ts: pd.Timestamp) -> str:
            ts = pd.to_datetime(ts)
            return f"{MES_MAP[ts.month]} {ts.year}"

        mes_sele = st.selectbox(
            "Mes de análisis (Cierre)",
            options=lista_opciones,
            index=default_idx,
            format_func=fmt_mes_abrev,
            key="mes_kpi_sucursal"
        )
    else: 
        # Por defecto: último mes completo disponible
        lista_opciones = (
            meses_completos_y["mes"]
            .sort_values(ascending=False)   # Más reciente primero
            .tolist()
        )

        default_idx = 0  # el más reciente

        def fmt_mes_abrev(ts: pd.Timestamp) -> str:
            ts = pd.to_datetime(ts)
            return f"{MES_MAP[ts.month]} {ts.year}"

        mes_sele = st.selectbox(
            "Mes de análisis (Cierre)",
            options=lista_opciones,
            index=default_idx,
            format_func=fmt_mes_abrev,
            key="mes_kpi_sucursal"
        )


    last_full_ini = mes_sele
    last_full_end = (last_full_ini + pd.offsets.MonthEnd(0)).normalize()

    prev_full_end = (last_full_ini - pd.Timedelta(days=1)).normalize()
    prev_full_ini = prev_full_end.replace(day=1)
    
    # ---------------------------------------------------------------------------
    # 2) Agregar df_folios a NIVEL MENSUAL POR SUCURSAL
    # ---------------------------------------------------------------------------
    df_folios["mes"] = df_folios["Fecha"].dt.to_period("M").dt.to_timestamp()

    monthly_suc = (
        df_folios
        .groupby(["mes", "NombreSucursal"], as_index=False)["ventas_subtotal"]
        .sum()
        .rename(columns={"ventas_subtotal": "ingreso"})
    )

    # ---------------------------------------------------------------------------
    # 3) Filtrar último mes completo y mes anterior
    # ---------------------------------------------------------------------------
    mask_last = (monthly_suc["mes"] >= last_full_ini) & (monthly_suc["mes"] <= last_full_end)
    mask_prev = (monthly_suc["mes"] >= prev_full_ini) & (monthly_suc["mes"] <= prev_full_end)

    last_by_suc = (
        monthly_suc.loc[mask_last]
        .groupby("NombreSucursal", as_index=False)["ingreso"]
        .sum()
        .rename(columns={"ingreso": "ing_last"})
    )

    prev_by_suc = (
        monthly_suc.loc[mask_prev]
        .groupby("NombreSucursal", as_index=False)["ingreso"]
        .sum()
        .rename(columns={"ingreso": "ing_prev"})
    )

    if last_by_suc.empty:
        st.info("No hay datos del último mes completo para calcular KPIs.")
        st.stop()

    merged = (
        last_by_suc
        .merge(prev_by_suc, on="NombreSucursal", how="outer")
        .fillna(0.0)
    )

    # ---------------------------------------------------------------------------
    # 4) KPIs PRINCIPALES
    # ---------------------------------------------------------------------------
    n_suc_last = max((merged["ing_last"] > 0).sum(), 1)
    n_suc_prev = max((merged["ing_prev"] > 0).sum(), 1)

    avg_last = merged["ing_last"].sum() / n_suc_last
    avg_prev = merged["ing_prev"].sum() / n_suc_prev

    suc_arriba_prom = (merged["ing_last"] > avg_last).sum()
    pct_vs_prev = (avg_last - avg_prev) / avg_prev if avg_prev > 0 else None

    k1, k2, k3, k4 = st.columns(4)

    with k1:
        st.metric(
            "Ingreso promedio por sucursal",
            f"${avg_last:,.2f}",
            delta=("NA" if pct_vs_prev is None else f"{pct_vs_prev:+.2%}")
        )
    with k2:
        st.metric(
            "Sucursales arriba del promedio",
            f"{suc_arriba_prom:,}"
        )
    with k3:
        st.metric(
            "Total red — último mes",
            f"${merged['ing_last'].sum():,.2f}"
        )
    with k4:
        st.metric(
            "Total red — mes anterior",
            f"${merged['ing_prev'].sum():,.2f}"
        )



    # ---------------------------------------------------------------------------
    # 5) TOP 3 sucursales (comparativo)
    # ---------------------------------------------------------------------------
    top_last = merged.sort_values("ing_last", ascending=False).head(3)
    top_prev = merged.sort_values("ing_prev", ascending=False).head(3)

    c1, c2 = st.columns(2)

    with c1:
        st.markdown(f"**Top 3 — {last_full_ini.strftime('%B %Y').capitalize()}**")
        for i, r in enumerate(top_last.itertuples(), 1):
            st.markdown(f"- **{i}. {r.NombreSucursal}** — ${r.ing_last:,.0f}")

    with c2:
        st.markdown(f"**Top 3 — {prev_full_ini.strftime('%B %Y').capitalize()}**")
        for i, r in enumerate(top_prev.itertuples(), 1):
            st.markdown(f"- **{i}. {r.NombreSucursal}** — ${r.ing_prev:,.0f}")

    # ---------------------------------------------------------------------------
    # 6) Dona de distribución (último mes completo)
    # ---------------------------------------------------------------------------
    c1, c2 = st.columns(2)

    with c1:
        with st_card("Distribución del ingreso por sucursal (último mes completo)"):

            pie_df = last_by_suc.sort_values("ing_last", ascending=False)

            N = 10
            if len(pie_df) > N:
                otros = pd.DataFrame({
                    "NombreSucursal": ["Otros"],
                    "ing_last": [pie_df["ing_last"].iloc[N:].sum()]
                })
                pie_df = pd.concat([pie_df.head(N), otros], ignore_index=True)

            fig_pie = px.pie(
                pie_df,
                names="NombreSucursal",
                values="ing_last",
                hole=0.55,
                color_discrete_sequence=PICTON_BLUE[::-1]
            )

            fig_pie.update_traces(
                textposition="inside",
                textinfo="percent+label",
                hovertemplate="Sucursal: %{label}<br>Ingreso: $%{value:,.2f}<extra></extra>"
            )
            fig_pie.update_layout(showlegend=False)

            titulo = f"Distribución de ingreso — {last_full_ini.strftime('%B %Y').capitalize()}"
            st.plotly_chart(apply_cabanna_theme(fig_pie, titulo), use_container_width=True)

    # ---------------------------------------------------------------------------
    # 7) Tabla soporte
    # ---------------------------------------------------------------------------
    with c2:
        det = merged.copy()
        det["Δ"] = det["ing_last"] - det["ing_prev"]
        det["%Δ"] = det.apply(lambda r: (r["Δ"] / r["ing_prev"]) if r["ing_prev"] > 0 else None, axis=1)

        show = det.rename(columns={
            "NombreSucursal": "Sucursal",
            "ing_last": f"Ingreso {last_full_ini:%Y-%m}",
            "ing_prev": f"Ingreso {prev_full_ini:%Y-%m}",
            "Δ": "Delta"
        })

        st.dataframe(
            show.style.format({
                f"Ingreso {last_full_ini:%Y-%m}": "${:,.2f}",
                f"Ingreso {prev_full_ini:%Y-%m}": "${:,.2f}",
                "Delta": "${:,.2f}",
                "%Δ": "{:+.2%}",
            }),
            use_container_width=True
        )


    # ===============================================================================
    # ===============================================================================
    # Índice de estabilidad (CV semanal por sucursal) — usando df_folios
    # ===============================================================================
    st.markdown(
        f"Índice de estabilidad por sucursal (CV semanal en {last_full_ini.strftime('%B %Y').capitalize()})"
    )

    if df_folios.empty:
        st.info("No hay datos suficientes para calcular la estabilidad.")
        st.stop()

    # ---------------------------------------------------------------------------
    # 0) Reducir al mínimo: solo columnas necesarias + solo rango necesario
    #    (esto SOLO ya baja mucho el tiempo)
    # ---------------------------------------------------------------------------
    cols = ["Fecha", "NombreSucursal", "ventas_subtotal"]

    # Importante: calcula máscara por fecha ANTES de crear semana
    f = df_folios["Fecha"]
    mask_range = (f >= last_full_ini) & (f <= last_full_end)

    df_w = df_folios.loc[mask_range, cols]
    if df_w.empty:
        st.info("No hay datos en el rango semanal seleccionado.")
        st.stop()

    # Tipos seguros (evita coerciones repetidas)
    df_w["ventas_subtotal"] = pd.to_numeric(df_w["ventas_subtotal"], errors="coerce").fillna(0.0)

    # ---------------------------------------------------------------------------
    # 1) Semana calendario (lunes como inicio) SIN apply
    #    Esto es clave: evita Period + apply
    # ---------------------------------------------------------------------------
    # Opción A (recomendada): Period semanal con inicio lunes y start_time vectorizado
    df_w = df_w.assign(
        semana=df_w["Fecha"].dt.to_period("W-MON").dt.start_time
    )

    weekly = (
        df_w.groupby(["NombreSucursal", "semana"], as_index=False, sort=False)["ventas_subtotal"]
            .sum()
            .rename(columns={"ventas_subtotal": "ingreso_sem"})
    )

    # ---------------------------------------------------------------------------
    # 2) KPIs por sucursal (media, std, CV)
    #    std en pandas por default ddof=1; para CV estable con pocas semanas,
    #    puedes usar ddof=0 (poblacional). Mantengo el default para no cambiar resultados.
    # ---------------------------------------------------------------------------
    grp = (
        weekly.groupby("NombreSucursal", as_index=False, sort=False)
            .agg(
                media=("ingreso_sem", "mean"),
                std=("ingreso_sem", "std"),
                semanas=("ingreso_sem", "count")
            )
    )

    # CV robusto
    media = grp["media"].to_numpy()
    std = grp["std"].to_numpy()
    cv = np.divide(std, media, out=np.zeros_like(std, dtype=float), where=(media > 0))
    grp["cv"] = cv
    grp = grp.fillna(0.0)

    c1s, c2s = st.columns(2)

    # ---------------------------------------------------------------------------
    # 3) Ranking CV
    # ---------------------------------------------------------------------------
    with c1s:
        rank = grp.sort_values("cv", ascending=True, kind="mergesort")

        fig_cv = px.bar(
            rank,
            x="cv",
            y="NombreSucursal",
            orientation="h",
            text=[f"{v:.2f}" for v in rank["cv"].to_numpy()],
        )

        fig_cv.update_traces(
            textposition="inside",
            insidetextanchor="middle",
            textfont=dict(color="white"),
            marker=dict(color=PICTON_BLUE[6]),
            customdata=rank[["semanas"]].to_numpy(),
            hovertemplate=(
                "Sucursal: %{y}<br>"
                "CV semanal: %{x:.2f}<br>"
                "Semanas analizadas: %{customdata[0]:,}"
                "<extra></extra>"
            ),
        )

        fig_cv.update_layout(
            title="Estabilidad semanal por sucursal (menor CV = más estable)",
            xaxis=dict(title="Coeficiente de Variación (CV)", color=COLORS["white"], showgrid=False, tickformat=".2f"),
            yaxis=dict(title=None, color=COLORS["white"], showgrid=False),
            height=520,
        )

        st.plotly_chart(apply_cabanna_theme(fig_cv), use_container_width=True)

    # ---------------------------------------------------------------------------
    # 4) Scatter
    # ---------------------------------------------------------------------------
    with c2s:
        fig_sc = px.scatter(
            grp,
            x="media",
            y="cv",
            text="NombreSucursal",
            labels={"media": "Ingreso promedio semanal", "cv": "Coeficiente de Variación (CV)"},
        )

        fig_sc.update_traces(
            marker=dict(size=10, color=PICTON_BLUE[6]),
            textposition="top center",
            hovertemplate=(
                "Sucursal: %{text}<br>"
                "Ingreso promedio semanal: $%{x:,.2f}<br>"
                "CV semanal: %{y:.2f}"
                "<extra></extra>"
            )
        )

        fig_sc.update_layout(
            title="Ingreso promedio semanal vs estabilidad",
            xaxis=dict(color=COLORS["white"], showgrid=False),
            yaxis=dict(color=COLORS["white"], showgrid=False),
            height=520,
        )

        st.plotly_chart(apply_cabanna_theme(fig_sc), use_container_width=True)


        st.info(
            "- Cada punto es una sucursal.\n"
            "- Eje X: **Ingreso promedio semanal**.\n"
            "- Eje Y: **CV semanal (volatilidad)**.\n\n"
            "**Lectura por cuadrantes:**\n"
            "- 🟢 Derecha–Abajo: alto ingreso y alta estabilidad (mejor desempeño).\n"
            "- 🟡 Derecha–Arriba: alto ingreso pero inestable.\n"
            "- 🔴 Izquierda–Arriba: bajo ingreso y alta volatilidad.\n"
            "- 🔵 Izquierda–Abajo: bajo ingreso pero estable."
        )



