Spaces:
Running
Running
| import io | |
| import os | |
| import shutil | |
| import zipfile | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Tuple | |
| from dotenv import load_dotenv | |
| import streamlit as st | |
| from reporter.generate import generate_selected, list_people | |
| load_dotenv() | |
| st.set_page_config(page_title="BDC Report Generator", layout="wide") | |
| def _require_login() -> None: | |
| """Login semplice via variabili d'ambiente (file .env). | |
| Variabili attese: | |
| - AUTH_USER | |
| - AUTH_PASSWORD | |
| Se non configurate, l'app resta accessibile (utile in sviluppo). | |
| """ | |
| user = os.getenv("AUTH_USER", "") | |
| pwd = os.getenv("AUTH_PASSWORD", "") | |
| if not user or not pwd: | |
| return | |
| if st.session_state.get("auth_ok"): | |
| return | |
| st.markdown("<br>" * 3, unsafe_allow_html=True) | |
| _, col, _ = st.columns([1, 1.2, 1]) | |
| with col: | |
| st.title("Bilancio Competenze") | |
| u = st.text_input("Username", key="_login_u", placeholder="inserisci username") | |
| p = st.text_input("Password", type="password", key="_login_p", placeholder="inserisci password") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| if st.button("Accedi", type="primary", use_container_width=True): | |
| if u == user and p == pwd: | |
| st.session_state.auth_ok = True | |
| st.rerun() | |
| else: | |
| st.error("Credenziali non valide", icon="π") | |
| st.stop() | |
| def _save_upload(upload, to_dir: Path) -> Optional[Path]: | |
| if upload is None: | |
| return None | |
| p = to_dir / upload.name | |
| p.write_bytes(upload.getbuffer()) | |
| return p | |
| def _zip_bytes(paths: List[Path]) -> bytes: | |
| buf = io.BytesIO() | |
| with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: | |
| for p in paths: | |
| if p.exists() and p.is_file(): | |
| z.write(p, arcname=p.name) | |
| return buf.getvalue() | |
| _require_login() | |
| make_pdf = True | |
| work_root = Path("/tmp/bdc_app") | |
| work_root.mkdir(parents=True, exist_ok=True) | |
| def _ensure_tmp() -> Path: | |
| if work_root.exists(): | |
| shutil.rmtree(work_root) | |
| work_root.mkdir(parents=True, exist_ok=True) | |
| return work_root | |
| def _load_people_lists( | |
| c_auto_p: Optional[Path], | |
| c_val_p: Optional[Path], | |
| m_auto_p: Optional[Path], | |
| m_val_p: Optional[Path], | |
| ) -> Tuple[List[str], List[str]]: | |
| collab_people: List[str] = [] | |
| manager_people: List[str] = [] | |
| if c_auto_p and c_val_p: | |
| collab_people = list_people(c_auto_p, c_val_p) | |
| if m_auto_p and m_val_p: | |
| manager_people = list_people(m_auto_p, m_val_p) | |
| return collab_people, manager_people | |
| # ββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with st.sidebar: | |
| st.title("βοΈ Impostazioni") | |
| sezioni = st.multiselect( | |
| "Tipologia", | |
| options=["Collaboratori", "Manager"], | |
| default=["Collaboratori", "Manager"], | |
| ) | |
| show_collab = "Collaboratori" in sezioni | |
| show_manager = "Manager" in sezioni | |
| if show_collab: | |
| st.divider() | |
| st.title("π₯ Collaboratori") | |
| tpl_collab = st.file_uploader("Modello Documento", type=["docx"], key="tpl_c") | |
| c_auto = st.file_uploader("Autovalutazione", type=["xlsx"], key="c_auto") | |
| c_val = st.file_uploader("Valutazione", type=["xlsx"], key="c_val") | |
| if all([tpl_collab, c_auto, c_val]): | |
| st.success("Files collaboratori inseriti", icon="β ") | |
| else: | |
| st.error("Inserire files collaboratori", icon="β") | |
| else: | |
| tpl_collab = c_auto = c_val = None | |
| if show_manager: | |
| st.divider() | |
| st.title("π’ Manager") | |
| tpl_manager = st.file_uploader("Modello Documento", type=["docx"], key="tpl_m") | |
| m_auto = st.file_uploader("Autovalutazione", type=["xlsx"], key="m_auto") | |
| m_val = st.file_uploader("Valutazione", type=["xlsx"], key="m_val") | |
| if all([tpl_manager, m_auto, m_val]): | |
| st.success("Files manager inseriti", icon="β ") | |
| else: | |
| st.error("Inserire files manager", icon="β") | |
| else: | |
| tpl_manager = m_auto = m_val = None | |
| collab_ready = all([tpl_collab, c_auto, c_val]) | |
| manager_ready = all([tpl_manager, m_auto, m_val]) | |
| can_prepare = collab_ready or manager_ready | |
| st.divider() | |
| if st.button("Genera Report", type="primary", disabled=not can_prepare, use_container_width=True): | |
| tmp = _ensure_tmp() | |
| tpl_c_p = _save_upload(tpl_collab, tmp) if collab_ready else None | |
| tpl_m_p = _save_upload(tpl_manager, tmp) if manager_ready else None | |
| c_auto_p = _save_upload(c_auto, tmp) if collab_ready else None | |
| c_val_p = _save_upload(c_val, tmp) if collab_ready else None | |
| m_auto_p = _save_upload(m_auto, tmp) if manager_ready else None | |
| m_val_p = _save_upload(m_val, tmp) if manager_ready else None | |
| collab_people, manager_people = _load_people_lists(c_auto_p, c_val_p, m_auto_p, m_val_p) | |
| if not collab_people and not manager_people: | |
| st.error("Non ho trovato nessun nome nei file. Controlla la colonna 'Nome e cognome'.") | |
| else: | |
| st.session_state._paths = { | |
| "tpl_c": str(tpl_c_p) if tpl_c_p else "", | |
| "tpl_m": str(tpl_m_p) if tpl_m_p else "", | |
| "c_auto": str(c_auto_p) if c_auto_p else "", | |
| "c_val": str(c_val_p) if c_val_p else "", | |
| "m_auto": str(m_auto_p) if m_auto_p else "", | |
| "m_val": str(m_val_p) if m_val_p else "", | |
| "tmp": str(tmp), | |
| } | |
| st.session_state._collab_people = collab_people | |
| st.session_state._manager_people = manager_people | |
| st.session_state._phase = "select" | |
| st.session_state._results_ready = False | |
| st.rerun() | |
| # ββ Main area ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.title("π Bilancio Competenze") | |
| with st.expander("βΉοΈ Funzionamento", expanded=True): | |
| st.markdown( | |
| """ | |
| Questa app genera report individuali di **Bilancio delle Competenze** in Word e PDF. | |
| ##### Istruzioni: | |
| 1. Nella barra laterale seleziona le sezioni da attivare (**Collaboratori**, **Manager**, o entrambe) | |
| 2. Carica il template Word e i due file Excel (autovalutazione e valutazione) per ciascuna sezione | |
| 3. Clicca **Genera Report** β scegli le persone da includere e scarica i report | |
| """ | |
| ) | |
| st.divider() | |
| phase = st.session_state.get("_phase") | |
| # ββ Fase: selezione persone ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if phase == "select": | |
| collab_people: List[str] = st.session_state.get("_collab_people", []) | |
| manager_people: List[str] = st.session_state.get("_manager_people", []) | |
| col_c, col_m = st.columns(2) | |
| sel_c: List[str] = [] | |
| sel_m: List[str] = [] | |
| with col_c: | |
| st.subheader(f"π₯ Collaboratori") | |
| for n in collab_people: | |
| if st.checkbox(n, value=True, key=f"c_{n}"): | |
| sel_c.append(n) | |
| with col_m: | |
| st.subheader(f"π’ Manager") | |
| for n in manager_people: | |
| if st.checkbox(n, value=True, key=f"m_{n}"): | |
| sel_m.append(n) | |
| tot = len(sel_c) + len(sel_m) | |
| c1, c2= st.columns([1, 1]) | |
| with c1: | |
| if st.button("Annulla", use_container_width=True): | |
| st.session_state._phase = None | |
| st.rerun() | |
| with c2: | |
| if st.button(f"Genera {tot} report", type="primary", disabled=tot == 0, use_container_width=True): | |
| st.session_state._selected_c = sel_c | |
| st.session_state._selected_m = sel_m | |
| st.session_state._phase = "generate" | |
| st.rerun() | |
| # ββ Fase: generazione βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| elif phase == "generate": | |
| sel_c = st.session_state.get("_selected_c", []) | |
| sel_m = st.session_state.get("_selected_m", []) | |
| total = len(sel_c) + len(sel_m) | |
| paths = st.session_state._paths | |
| def _p(key: str) -> Optional[Path]: | |
| v = paths.get(key, "") | |
| return Path(v) if v else None | |
| tpl_c_p, tpl_m_p = _p("tpl_c"), _p("tpl_m") | |
| c_auto_p, c_val_p = _p("c_auto"), _p("c_val") | |
| m_auto_p, m_val_p = _p("m_auto"), _p("m_val") | |
| out_dir = Path(paths["tmp"]) / "output" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| header = st.empty() | |
| header.subheader(f"Generazione in corso⦠(0/{total})") | |
| progress = st.progress(0) | |
| log_area = st.empty() | |
| all_produced = [] | |
| all_warnings: List[str] = [] | |
| done = 0 | |
| log_lines: List[str] = [] | |
| for name in sel_c: | |
| log_lines.append(f"β³ Collaboratore: **{name}**") | |
| log_area.markdown("\n\n".join(log_lines)) | |
| r = generate_selected( | |
| collab_auto=c_auto_p, collab_valut=c_val_p, collab_template=tpl_c_p, | |
| manager_auto=None, manager_valut=None, manager_template=None, | |
| selected_collaboratori=[name], selected_manager=[], | |
| output_dir=out_dir, make_pdf=make_pdf, | |
| ) | |
| all_produced.extend(r.produced) | |
| all_warnings.extend(r.warnings) | |
| done += 1 | |
| log_lines[-1] = f"β Collaboratore: **{name}**" | |
| log_area.markdown("\n\n".join(log_lines)) | |
| progress.progress(done / total) | |
| header.subheader(f"Generazione in corso⦠({done}/{total})") | |
| for name in sel_m: | |
| log_lines.append(f"β³ Manager: **{name}**") | |
| log_area.markdown("\n\n".join(log_lines)) | |
| r = generate_selected( | |
| collab_auto=None, collab_valut=None, collab_template=None, | |
| manager_auto=m_auto_p, manager_valut=m_val_p, manager_template=tpl_m_p, | |
| selected_collaboratori=[], selected_manager=[name], | |
| output_dir=out_dir, make_pdf=make_pdf, | |
| ) | |
| all_produced.extend(r.produced) | |
| all_warnings.extend(r.warnings) | |
| done += 1 | |
| log_lines[-1] = f"β Manager: **{name}**" | |
| log_area.markdown("\n\n".join(log_lines)) | |
| progress.progress(done / total) | |
| header.subheader(f"Generazione in corso⦠({done}/{total})") | |
| header.subheader(f"Completato β {total} report generati β ") | |
| st.session_state._results = [ | |
| {"person": a.person, "kind": a.kind, | |
| "docx_path": a.docx_path, "pdf_path": a.pdf_path, "notes": a.notes} | |
| for a in all_produced | |
| ] | |
| st.session_state._result_warnings = all_warnings | |
| st.session_state._results_ready = True | |
| st.session_state._phase = "results" | |
| st.rerun() | |
| # ββ Fase: risultati ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| elif phase == "results": | |
| results = st.session_state._results | |
| warnings = st.session_state.get("_result_warnings", []) | |
| if warnings: | |
| st.warning("\n".join(warnings)) | |
| st.success(f"Generazione completata: {len(results)} report", icon="β ") | |
| all_files: List[Path] = [] | |
| by_kind: Dict[str, list] = {} | |
| for a in sorted(results, key=lambda x: (x["kind"], x["person"])): | |
| by_kind.setdefault(a["kind"], []).append(a) | |
| def _render_table(artifacts: list) -> None: | |
| # Intestazione | |
| h0, h1, h2 = st.columns([4, 1, 1]) | |
| h0.markdown("**Persona**") | |
| h1.markdown("**Word**") | |
| h2.markdown("**PDF**") | |
| for a in artifacts: | |
| c0, c1, c2 = st.columns([4, 1, 1]) | |
| label = a["person"] | |
| if a["notes"]: | |
| label += f" \n*{a['notes']}*" | |
| c0.markdown(label) | |
| docx_path = Path(a["docx_path"]) | |
| all_files.append(docx_path) | |
| c1.download_button( | |
| "DOCX", | |
| data=docx_path.read_bytes(), | |
| file_name=docx_path.name, | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| use_container_width=True, | |
| key=f"docx_{a['kind']}_{a['person']}", | |
| ) | |
| has_pdf = a["pdf_path"] and Path(a["pdf_path"]).exists() | |
| if has_pdf: | |
| pdf_path = Path(a["pdf_path"]) | |
| all_files.append(pdf_path) | |
| c2.download_button( | |
| "PDF", | |
| data=pdf_path.read_bytes(), | |
| file_name=pdf_path.name, | |
| mime="application/pdf", | |
| use_container_width=True, | |
| key=f"pdf_{a['kind']}_{a['person']}", | |
| type="primary", | |
| ) | |
| kinds = list(by_kind.keys()) | |
| if len(kinds) > 1: | |
| tabs = st.tabs(kinds) | |
| for tab, kind in zip(tabs, kinds): | |
| with tab: | |
| _render_table(by_kind[kind]) | |
| else: | |
| st.subheader(kinds[0]) | |
| _render_table(by_kind[kinds[0]]) | |
| st.divider() | |
| c1, c2= st.columns([1, 1]) | |
| with c1: | |
| if st.button("Annulla", use_container_width=True): | |
| st.session_state._phase = None | |
| st.rerun() | |
| with c2: | |
| st.download_button( | |
| "Scarica Tutti i Files", | |
| data=_zip_bytes(all_files), | |
| file_name="BDC_report_output.zip", | |
| mime="application/zip", | |
| use_container_width=True, | |
| key="zip_all", | |
| type="primary", | |
| ) | |