Spaces:
Sleeping
Sleeping
| import os | |
| import io | |
| import unicodedata | |
| import pandas as pd | |
| import streamlit as st | |
| import plotly.express as px | |
| # ----------------------------- | |
| # Configuración de la página | |
| # ----------------------------- | |
| st.set_page_config( | |
| page_title="Employee Count Dashboard", | |
| page_icon="📊", | |
| layout="wide", | |
| ) | |
| # ----------------------------- | |
| # Utilidades | |
| # ----------------------------- | |
| def normalize(s: str) -> str: | |
| s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("utf-8", "ignore") | |
| return " ".join(s.lower().split()) | |
| def find_target_column(df: pd.DataFrame, target="extskhis_emp full name") -> str | None: | |
| norm_map = {col: normalize(col) for col in df.columns} | |
| for col, norm in norm_map.items(): | |
| if norm == normalize(target): | |
| return col | |
| candidates = [c for c, n in norm_map.items() if "full" in n and "name" in n] | |
| return candidates[0] if candidates else None | |
| def load_csv(file_obj) -> pd.DataFrame: | |
| return pd.read_csv(file_obj) | |
| def load_sample(sample_path: str) -> pd.DataFrame | None: | |
| if os.path.exists(sample_path): | |
| return pd.read_csv(sample_path) | |
| return None | |
| def pretty_number(n: int) -> str: | |
| return f"{n:,}".replace(",", " ") | |
| # ----------------------------- | |
| # Estilos (UI) | |
| # ----------------------------- | |
| CUSTOM_CSS = """ | |
| <style> | |
| .stApp { background-color: #0d1117; color: #e5e7eb; } | |
| .block-container { padding-top: 1.5rem; } | |
| /* Tarjetas KPI */ | |
| .kpi-card { | |
| border-radius: 14px; | |
| padding: 18px 20px; | |
| background: #161b22; | |
| border: 1px solid #30363d; | |
| box-shadow: 0 8px 16px -10px rgba(0,0,0,0.8); | |
| } | |
| .kpi-label { font-size: 0.85rem; color: #9ca3af; margin-bottom: 6px; } | |
| .kpi-value { font-size: 1.6rem; font-weight: 700; color: #f9fafb; } | |
| /* Sección principal */ | |
| .section-card { | |
| border-radius: 16px; | |
| padding: 20px; | |
| background: #161b22; | |
| border: 1px solid #30363d; | |
| box-shadow: 0 12px 24px -16px rgba(0,0,0,0.9); | |
| } | |
| /* Título con acento */ | |
| h1 span.accent { | |
| background: linear-gradient(90deg, #60a5fa, #34d399, #fbbf24); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| </style> | |
| """ | |
| st.markdown(CUSTOM_CSS, unsafe_allow_html=True) | |
| # ----------------------------- | |
| # Sidebar (carga de datos) | |
| # ----------------------------- | |
| st.sidebar.title("⚙️ Configuración") | |
| uploaded = st.sidebar.file_uploader("Sube tu archivo CSV", type=["csv"]) | |
| sample_note = st.sidebar.empty() | |
| df = None | |
| source_label = "" | |
| if uploaded is not None: | |
| try: | |
| data_bytes = uploaded.getvalue() | |
| df = load_csv(io.BytesIO(data_bytes)) | |
| source_label = f"Fuente: Archivo subido — **{uploaded.name}**" | |
| except Exception as e: | |
| st.sidebar.error(f"Error al leer el CSV: {e}") | |
| else: | |
| df_sample = load_sample("data/sample.csv") | |
| if df_sample is not None: | |
| df = df_sample | |
| source_label = "Fuente: `data/sample.csv` (muestra)" | |
| sample_note.info("No subiste archivo. Mostrando ejemplo.") | |
| else: | |
| sample_note.warning("No subiste archivo y no existe `data/sample.csv`. Sube un CSV para continuar.") | |
| # ----------------------------- | |
| # UI principal | |
| # ----------------------------- | |
| st.title("📊 Employee Count Dashboard", anchor=False) | |
| if df is None or df.empty: | |
| st.info("Sube un CSV con la columna **EXTSKHIS_EMP FULL NAME** para ver el dashboard.") | |
| st.stop() | |
| target_col = find_target_column(df, "EXTSKHIS_EMP FULL NAME") | |
| if target_col is None: | |
| st.error("No se encontró la columna requerida.") | |
| st.write("Columnas detectadas:", list(df.columns)) | |
| st.stop() | |
| df[target_col] = df[target_col].astype(str).str.strip() | |
| # ----------------------------- | |
| # Controles | |
| # ----------------------------- | |
| with st.sidebar: | |
| st.divider() | |
| st.subheader("Filtros") | |
| search = st.text_input("Filtrar por nombre", placeholder="Ej: Maria, Juan...") | |
| min_count = st.number_input("Mínimo de ocurrencias", min_value=1, value=1, step=1) | |
| top_n = st.slider("Mostrar Top N", min_value=5, max_value=100, value=20, step=5) | |
| sort_mode = st.radio("Orden", ["Por conteo (desc)", "Alfabético (A→Z)"], index=0) | |
| df_filtered = df | |
| if search: | |
| s = search.lower() | |
| df_filtered = df[df[target_col].str.lower().str.contains(s, na=False)] | |
| counts = df_filtered.groupby(target_col, dropna=False).size().reset_index(name="Count") | |
| counts = counts[counts["Count"] >= min_count] | |
| if sort_mode == "Por conteo (desc)": | |
| counts = counts.sort_values("Count", ascending=False) | |
| else: | |
| counts = counts.sort_values(target_col, ascending=True) | |
| counts_top = counts.head(top_n) | |
| # ----------------------------- | |
| # KPIs | |
| # ----------------------------- | |
| c1, c2, c3 = st.columns(3) | |
| with c1: | |
| st.markdown("<div class='kpi-card'>" | |
| "<div class='kpi-label'>Registros totales</div>" | |
| f"<div class='kpi-value'>{pretty_number(len(df))}</div>" | |
| "</div>", unsafe_allow_html=True) | |
| with c2: | |
| st.markdown("<div class='kpi-card'>" | |
| "<div class='kpi-label'>Nombres únicos</div>" | |
| f"<div class='kpi-value'>{pretty_number(counts[target_col].nunique())}</div>" | |
| "</div>", unsafe_allow_html=True) | |
| with c3: | |
| st.markdown("<div class='kpi-card'>" | |
| "<div class='kpi-label'>Mostrando en gráfico</div>" | |
| f"<div class='kpi-value'>{pretty_number(len(counts_top))}</div>" | |
| "</div>", unsafe_allow_html=True) | |
| st.caption(source_label) | |
| # ----------------------------- | |
| # Gráfico (vertical, colorido, fondo negro) | |
| # ----------------------------- | |
| st.markdown("### 🎨 Unidades hechas por cada operador") | |
| if counts_top.empty: | |
| st.warning("No hay filas que cumplan los filtros actuales.") | |
| else: | |
| fig = px.bar( | |
| counts_top, | |
| x=target_col, | |
| y="Count", | |
| text="Count", | |
| color=target_col, # cada barra con color distinto | |
| color_discrete_sequence=px.colors.qualitative.Bold, # paleta llamativa | |
| height=600, | |
| ) | |
| fig.update_traces(textposition="outside") | |
| fig.update_layout( | |
| xaxis_title="Nombre", | |
| yaxis_title="Conteo", | |
| plot_bgcolor="#0d1117", | |
| paper_bgcolor="#0d1117", | |
| font=dict(color="white"), | |
| margin=dict(l=10, r=10, t=30, b=50), | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ----------------------------- | |
| # Tabla | |
| # ----------------------------- | |
| with st.expander("📄 Ver tabla de conteos"): | |
| st.dataframe(counts.reset_index(drop=True), use_container_width=True) | |
| # ----------------------------- | |
| # Descargar | |
| # ----------------------------- | |
| csv_bytes = counts.to_csv(index=False).encode("utf-8") | |
| st.download_button( | |
| "⬇️ Descargar conteos (CSV)", | |
| data=csv_bytes, | |
| file_name="employee_counts.csv", | |
| mime="text/csv", | |
| ) |