|
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=1 |
|
) |
|
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(str).map(len).max() |
|
max_length = max(max_length, len(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) |
|
|