| import pandas as pd |
| import numpy as np |
| import streamlit as st |
| import streamlit as ste |
| from typing import Optional, List, Dict, Any |
|
|
| from st_aggrid import AgGrid |
| from st_aggrid.grid_options_builder import GridOptionsBuilder |
|
|
| from io import BytesIO |
| import xlsxwriter |
| from datetime import date, datetime |
|
|
| from helpertools_TM import helpers |
|
|
| helper = helpers() |
|
|
|
|
| class makeInterface: |
| """ |
| A class to create and manage the user interface for data validation and processing. |
| |
| This class handles file uploads, creates the interface layout, manages validation checks, |
| and provides functionality for displaying and downloading results. |
| |
| Attributes: |
| valori: Configuration values and constants for the interface |
| ANNO_RIFERIMENTO (str): The reference year for data validation |
| DEBUGWARNINGS (bool): Flag for showing debug warnings |
| TOLLERANZAZERO (bool): Flag for zero tolerance mode |
| ERRORDICT (dict): Dictionary of error types and descriptions |
| ERROR_ICONS (dict): Dictionary mapping error types to their icons |
| """ |
|
|
| def __init__(self, valori): |
| """ |
| Initialize the interface with configuration values. |
| |
| Args: |
| valori: Object containing configuration values and constants |
| """ |
| self.valori = valori |
| |
| |
| |
| st.sidebar.markdown('<div style="margin-top: 220px;"></div>', unsafe_allow_html=True) |
| |
| self.ANNO_RIFERIMENTO = st.sidebar.selectbox( |
| "ANNO RIFERIMENTO", (["2023","2024", "2025", "2026"]), index=2 |
| ) |
| self.make_sidebar() |
| self.DEBUGWARNINGS = False |
| self.TOLLERANZAZERO = False |
|
|
| self.STATUS = st.sidebar.empty() |
| self.STATUS2 = st.sidebar.empty() |
| self.STATUS3 = st.sidebar.empty() |
|
|
| try: |
| self.ERRORDICT = valori.ERRORDICT[self.ANNO_RIFERIMENTO] |
| self.ERROR_ICONS = valori.ERROR_ICONS |
| except Exception as e: |
| self.STATUS.error( |
| f"Non e' stato possibile creare la lista dei controlli. Forse non sono stati definiti per l'anno {self.ANNO_RIFERIMENTO}? Errore: {e}" |
| ) |
| if self.TOLLERANZAZERO or self.DEBUGWARNINGS: |
| st.error("Elaborazione interrotta") |
| st.stop() |
|
|
| self.make_form() |
|
|
| |
| self.upload_all_files() |
| self.choose_checks() |
| self.apply_custom_theme() |
|
|
| def apply_custom_theme(self): |
| """Apply custom styling to improve the overall appearance of the interface.""" |
| st.markdown(""" |
| <style> |
| .main { |
| padding: 1rem 1rem; |
| } |
| .stTabs [data-baseweb="tab-list"] { |
| gap: 10px; |
| } |
| .stTabs [data-baseweb="tab"] { |
| height: 50px; |
| white-space: pre-wrap; |
| border-radius: 5px 5px 0px 0px; |
| padding: 10px 16px; |
| background-color: #f0f2f6; |
| } |
| .stTabs [aria-selected="true"] { |
| background-color: #4e8cff !important; |
| color: white !important; |
| } |
| /* Success message styling */ |
| div.element-container div[data-testid="stAlert"] { |
| border-radius: 8px; |
| } |
| /* Button styling */ |
| .stButton button { |
| border-radius: 6px; |
| font-weight: 500; |
| padding: 0.5rem 1rem; |
| transition: all 0.3s; |
| } |
| .stButton button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| def get_version(self) -> str: |
| return "Interface 1.0.0" |
|
|
| def make_sidebar(self): |
| """ |
| Create the sidebar interface with configuration options. |
| |
| Creates checkboxes and radio buttons for various settings including: |
| - Integrity message display |
| - Date string conversion |
| - Processing interruption settings |
| - Constants display |
| - General debug mode |
| """ |
|
|
| |
| |
| with st.sidebar.expander("Configurazione"): |
| self.COMUNETEDESCO = st.checkbox( |
| "Mostra comuni in tedesco", False |
| ) |
| self.MOSTRAERRORIINTEGRITA = st.checkbox( |
| "Mostra messaggi integrita' strutturale", True |
| ) |
| self.CONVERTIDATESTRINGHE = st.checkbox( |
| "Convertire le date in stringhe", True |
| ) |
| self.INTERRUZIONE = st.radio( |
| "Interrompere elaborazione se...", |
| ("Non interrompere", "Warnings", "Tolleranza zero"), |
| ) |
|
|
| if self.INTERRUZIONE == "Warnings": |
| self.DEBUGWARNINGS = True |
| elif self.INTERRUZIONE == "Tolleranza zero": |
| self.TOLLERANZAZERO = True |
|
|
| self.MOSTRACOSTANTI = st.checkbox("Mostra costanti per anno riferimento") |
| if self.MOSTRACOSTANTI: |
| try: |
| st.info(self.valori.COSTANTI[self.ANNO_RIFERIMENTO]) |
| except: |
| st.error("Impossibile mostrare costanti") |
|
|
| self.DEBUGGENERAL = st.checkbox("Debug generale") |
|
|
|
|
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def make_expander(self, title1, title2, count, dftemp): |
| """ |
| Create an expandable section to display data results. |
| |
| Args: |
| title1 (str): Primary title for the expander |
| title2 (str): Secondary title/description |
| count (int): Number of occurrences found |
| dftemp (pd.DataFrame): DataFrame containing the results to display |
| """ |
| with st.expander(f"Trovate **{count}** occorrenze per {title1}"): |
| st.info(title2) |
| |
| |
| self.make_grid( |
| dftemp.sort_values(by=["Cognome", "Nome"]), |
| title1, |
| ) |
|
|
| self.download_excel_file( |
| dftemp.sort_values(by=["Cognome", "Nome"]), |
| f"{title1}.xlsx", |
| ) |
|
|
| def make_form(self) -> None: |
| """ |
| Create a sidebar form with a submit button. |
| |
| The form includes a submit button that triggers the start of data processing and error checking. |
| """ |
| frm = st.sidebar.form("Auswahl", clear_on_submit=True) |
| with frm: |
| |
| |
| self.submit = frm.form_submit_button("Iniziare elaborazione e controlli") |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| def choose_checks(self) -> None: |
| """ |
| Creates a dynamic list of checkboxes based on a dictionary containing error names and descriptions. |
| Updates the 'checks' dictionary with the selected checkboxes. |
| |
| :return: None |
| """ |
| |
| self.checks: Dict[str, bool] = {} |
|
|
| |
| expndr = st.expander( |
| "SELEZIONE CONTROLLI DA ESEGUIRE (i controlli sull'integrità dei dati vengono eseguiti sempre)", |
| expanded=False, |
| ) |
|
|
| try: |
| with expndr: |
| |
| select_all = st.checkbox( |
| "🗂️ Selezionare/deselezionare tutti i controlli", |
| value=True, |
| key="select_all_checks", |
| ) |
|
|
| |
| columns = expndr.columns(4) |
|
|
| |
| for idx, (error_key, error_description) in enumerate( |
| self.ERRORDICT.items() |
| ): |
| |
| col = columns[idx % 4] |
|
|
| |
| icon = self.ERROR_ICONS.get(error_key, "⚠️") |
|
|
| |
| self.checks[error_key] = col.checkbox( |
| label=f"{icon} {error_description}", |
| value=select_all, |
| key=f"{error_key}_checkbox", |
| ) |
|
|
| except Exception as e: |
| with expndr: |
| |
| st.warning( |
| f"Non è stato possibile creare la lista dei controlli. Forse non sono stati definiti per l'anno {self.ANNO_RIFERIMENTO}? Errore: {e}" |
| ) |
|
|
| |
| if getattr(self, "TOLLERANZAZERO", False) or getattr( |
| self, "DEBUGWARNINGS", False |
| ): |
| st.error("Elaborazione interrotta") |
| st.stop() |
|
|
| def make_tabs(self) -> None: |
| """ |
| Create the main navigation tabs for the application. |
| |
| Creates three tabs: |
| - Data Integrity |
| - Errors and Inconsistencies |
| - Final Tables |
| """ |
| self.TAB1, self.TAB2, self.TAB3 = st.tabs( |
| [ |
| "INTEGRITÀ DATI", |
| "ERRORI e INCONGRUENZE RISCONTRATI", |
| "TABELLE FINALI", |
| ] |
| ) |
|
|
| @st.fragment() |
| def buildGrid(self, data: pd.DataFrame): |
| """ |
| Build a grid configuration for displaying tabular data. |
| |
| Args: |
| data (pd.DataFrame): The DataFrame to be displayed in the grid |
| |
| Returns: |
| dict: Grid options configuration for AG-Grid |
| """ |
| gb = GridOptionsBuilder.from_dataframe(data) |
| |
| gb.configure_pagination(paginationAutoPageSize=True, paginationPageSize=20) |
| |
| gb.configure_side_bar() |
| |
| gb.configure_selection(selection_mode="multiple", use_checkbox=True) |
| |
| gb.configure_default_column( |
| groupable=True, |
| value=True, |
| enableRowGroup=True, |
| aggFunc="count", |
| editable=True, |
| ) |
|
|
| gridOptions = gb.build() |
| return gridOptions |
|
|
| @st.fragment() |
| def make_grid(self, dff: pd.DataFrame, k: str) -> None: |
| """ |
| Create and display an interactive data grid. |
| |
| Args: |
| dff (pd.DataFrame): DataFrame to be displayed in the grid |
| k (str): Unique key identifier for the grid component |
| """ |
| |
| |
| helper = helpers() |
| dff = helper.drop_columns(dff, self.ERRORDICT) |
| |
| |
| new_row_df = pd.DataFrame({"filename": " "}, index=[0]) |
| dff = pd.concat([dff, new_row_df], ignore_index=True) |
|
|
| |
| new_row_data = { |
| "filename": "file generato in data: " + date.today().strftime("%d/%m/%Y") |
| } |
| new_row_df = pd.DataFrame(new_row_data, index=[0]) |
| dff = pd.concat([dff, new_row_df], ignore_index=True) |
|
|
| |
| gridOptions = self.buildGrid(dff) |
|
|
| |
| AgGrid(dff, gridOptions=gridOptions, enable_enterprise_modules=True, key=k) |
|
|
| @st.fragment() |
| def download_excel_file(self, dftosave: pd.DataFrame, nome_file: str, flag: Optional[str] = None) -> None: |
| """ |
| Create and provide a download button for an Excel file with improved styling. |
| |
| Args: |
| dftosave (pd.DataFrame): DataFrame to be saved as Excel |
| nome_file (str): Name of the output file |
| flag (Optional[str]): Flag to control column handling ('mantieniColonneErrore' to keep error columns) |
| |
| Raises: |
| KeyError: If required columns are missing from the DataFrame |
| ValueError: If there are invalid or incompatible values |
| Exception: For other unexpected errors |
| """ |
| try: |
| |
| dftosave = dftosave.drop( |
| columns=["Colonna_L_da_cancellare_dopo"], errors="ignore" |
| ) |
|
|
| |
| timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") |
| nome_file = f"{timestamp}_{nome_file}" |
|
|
| |
| if flag == "mantieniColonneErrore": |
| pass |
| else: |
| dftosave = helper.drop_columns(dftosave, self.ERRORDICT) |
|
|
| |
| buffer = BytesIO() |
|
|
| |
| with pd.ExcelWriter(buffer, engine="xlsxwriter") as writer: |
| dftosave.to_excel(writer, sheet_name="risultato", index=False) |
| workbook = writer.book |
| worksheet = writer.sheets["risultato"] |
|
|
| |
| header_format = workbook.add_format({ |
| 'bold': True, |
| 'bg_color': '#4E8CFF', |
| 'color': 'white', |
| 'border': 1, |
| 'align': 'center' |
| }) |
| |
| |
| data_format = workbook.add_format({ |
| 'border': 1 |
| }) |
| |
| |
| for col_num, value in enumerate(dftosave.columns.values): |
| worksheet.write(0, col_num, value, header_format) |
|
|
| |
| for idx, col in enumerate(dftosave.columns): |
| |
| max_length = dftosave[col].astype(object).fillna('').astype(str).map(len).max() |
| max_length = max(max_length, len(str(col))) + 2 |
| worksheet.set_column(idx, idx, max_length, data_format) |
| |
| |
| worksheet.autofilter(0, 0, len(dftosave), len(dftosave.columns) - 1) |
|
|
| |
| buffer.seek(0) |
| file_bytes = buffer.read() |
|
|
| |
| col1, col2 = st.columns([1, 2]) |
| |
| with col1: |
| |
| ste.download_button( |
| label="📥 Scarica Excel", |
| data=file_bytes, |
| file_name=nome_file, |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| use_container_width=True, |
| ) |
| |
| with col2: |
| st.markdown(f""" |
| <div style="padding: 10px; border-radius: 4px; background-color: #f0f2f6; font-size: 0.9rem;"> |
| <strong>File:</strong> {nome_file}<br> |
| <strong>Righe:</strong> {len(dftosave)} | <strong>Colonne:</strong> {len(dftosave.columns)} |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| except KeyError as ke: |
| st.error(f"Chiave mancante nel DataFrame: {ke}") |
| st.stop() |
|
|
| except ValueError as ve: |
| st.error(f"Valore non valido o incompatibile: {ve}") |
| st.stop() |
|
|
| except Exception as e: |
| st.error(f"Si è verificato un errore inaspettato: {e}") |
| st.stop() |
|
|
| def upload_all_files(self) -> None: |
| """ |
| Allow users to upload all Excel files at once, automatically identifying the Allegato 666 file. |
| Enhanced with better visual styling and a single clear upload area. |
| """ |
| st.markdown("### 📁 Upload file Excel") |
| |
| |
| st.markdown(""" |
| <style> |
| /* Style the file uploader */ |
| .stFileUploader { |
| padding: 0 !important; |
| } |
| |
| /* Style the file uploader box - the actual drop area */ |
| .stFileUploader > div > div { |
| padding: 2rem 1rem 1rem 1rem !important; |
| border: 2px dashed #4e8cff !important; |
| border-radius: 10px !important; |
| } |
| |
| /* Add the instruction text directly within the file uploader area */ |
| .stFileUploader > div > div::before { |
| content: "Trascina qui i file Excel o fai clic per selezionarli" !important; |
| display: block !important; |
| color: #4e8cff !important; |
| text-align: center !important; |
| font-size: 1rem !important; |
| margin-bottom: 0.5rem !important; |
| font-weight: 500 !important; |
| } |
| |
| /* Add the secondary instruction text */ |
| .stFileUploader > div > div::after { |
| content: "(Incluso Allegato 666)" !important; |
| display: block !important; |
| color: #4e8cff !important; |
| text-align: center !important; |
| font-size: 0.8rem !important; |
| font-style: italic !important; |
| } |
| |
| /* Hide the uploaded file list that Streamlit shows by default */ |
| .stFileUploader [data-testid="stFileUploaderDropzone"] + div { |
| display: none !important; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
| |
| |
| all_uploaded_files = st.file_uploader( |
| "CaricaExcelInclusoAllegato666)", |
| accept_multiple_files=True, |
| label_visibility="collapsed" |
| ) |
| |
| |
| self.uploaded_files = [] |
| self.uploaded_allegato666 = None |
| |
| |
| if all_uploaded_files: |
| |
| valid_extensions = ['.xlsx', '.xls'] |
| |
| |
| valid_files = [] |
| invalid_files = [] |
| |
| for file in all_uploaded_files: |
| |
| file_extension = '.' + file.name.split('.')[-1].lower() |
| if file_extension in valid_extensions: |
| valid_files.append(file) |
| else: |
| invalid_files.append(file) |
| |
| |
| if invalid_files: |
| invalid_file_names = [file.name for file in invalid_files] |
| st.markdown(f""" |
| <div style="background-color: #ffdcdc; padding: 10px; border-radius: 8px; border-left: 5px solid #d32f2f; margin-bottom: 10px;"> |
| <p style="margin:0"><strong>❌ Errore:</strong> I seguenti file non sono in formato Excel e non verranno elaborati:</p> |
| <ul style="margin-top: 5px; margin-bottom: 0;"> |
| {''.join([f'<li>{name}</li>' for name in invalid_file_names])} |
| </ul> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| allegato_666_files = [] |
| other_files = [] |
| |
| for file in valid_files: |
| if "666" in file.name: |
| allegato_666_files.append(file) |
| else: |
| other_files.append(file) |
| |
| |
| self.uploaded_files = other_files |
| |
| |
| if len(allegato_666_files) > 0 or len(other_files) > 0: |
| col1, col2 = st.columns(2) |
| |
| |
| with col1: |
| if len(allegato_666_files) == 1: |
| self.uploaded_allegato666 = allegato_666_files[0] |
| st.markdown(f""" |
| <div style="background-color: #d1f0d9; padding: 10px; border-radius: 8px; border-left: 5px solid #2e7d32;"> |
| <p style="margin:0"><strong>✅ Allegato 666:</strong> {self.uploaded_allegato666.name}</p> |
| </div> |
| """, unsafe_allow_html=True) |
| elif len(allegato_666_files) > 1: |
| st.markdown(f""" |
| <div style="background-color: #ffdcdc; padding: 10px; border-radius: 8px; border-left: 5px solid #d32f2f;"> |
| <p style="margin:0"><strong>❌ Errore:</strong> Più file contengono '666' nel nome ({len(allegato_666_files)})</p> |
| </div> |
| """, unsafe_allow_html=True) |
| self.uploaded_allegato666 = None |
| else: |
| st.markdown(f""" |
| <div style="background-color: #fff8e1; padding: 10px; border-radius: 8px; border-left: 5px solid #ff9800;"> |
| <p style="margin:0"><strong>⚠️ Attenzione:</strong> Allegato 666 mancante</p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| with col2: |
| if len(other_files) > 0: |
| st.markdown(f""" |
| <div style="background-color: #d1f0d9; padding: 10px; border-radius: 8px; border-left: 5px solid #1976d2;"> |
| <p style="margin:0"><strong>ℹ️ Elenchi caricati:</strong> {len(other_files)}</p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| with st.expander("Mostra file caricati"): |
| for file in other_files: |
| st.markdown(f"📄 {file.name}") |
| else: |
| st.markdown(f""" |
| <div style="background-color: #fff8e1; padding: 10px; border-radius: 8px; border-left: 5px solid #ff9800;"> |
| <p style="margin:0"><strong>⚠️ Attenzione:</strong> Nessun file Excel aggiuntivo</p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown("<div style='margin-bottom: 25px;'></div>", unsafe_allow_html=True) |
|
|