MatteoScript commited on
Commit
4a4c518
Β·
verified Β·
1 Parent(s): dbd56c7

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +376 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,378 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
 
 
 
 
 
4
  import streamlit as st
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import shutil
4
+ import zipfile
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ from dotenv import load_dotenv
9
  import streamlit as st
10
 
11
+ from reporter.generate import generate_selected, list_people
12
+
13
+ load_dotenv()
14
+
15
+
16
+ st.set_page_config(page_title="BDC Report Generator", layout="wide")
17
+
18
+
19
+ def _require_login() -> None:
20
+ """Login semplice via variabili d'ambiente (file .env).
21
+
22
+ Variabili attese:
23
+ - AUTH_USER
24
+ - AUTH_PASSWORD
25
+
26
+ Se non configurate, l'app resta accessibile (utile in sviluppo).
27
+ """
28
+ user = os.getenv("AUTH_USER", "")
29
+ pwd = os.getenv("AUTH_PASSWORD", "")
30
+
31
+ if not user or not pwd:
32
+ return
33
+
34
+ if st.session_state.get("auth_ok"):
35
+ return
36
+
37
+ st.markdown("<br>" * 3, unsafe_allow_html=True)
38
+ _, col, _ = st.columns([1, 1.2, 1])
39
+ with col:
40
+ st.title("Bilancio Competenze")
41
+ u = st.text_input("Username", key="_login_u", placeholder="inserisci username")
42
+ p = st.text_input("Password", type="password", key="_login_p", placeholder="inserisci password")
43
+ st.markdown("<br>", unsafe_allow_html=True)
44
+ if st.button("Accedi", type="primary", use_container_width=True):
45
+ if u == user and p == pwd:
46
+ st.session_state.auth_ok = True
47
+ st.rerun()
48
+ else:
49
+ st.error("Credenziali non valide", icon="πŸ”’")
50
+
51
+ st.stop()
52
+
53
+
54
+ def _save_upload(upload, to_dir: Path) -> Optional[Path]:
55
+ if upload is None:
56
+ return None
57
+ p = to_dir / upload.name
58
+ p.write_bytes(upload.getbuffer())
59
+ return p
60
+
61
+
62
+ def _zip_bytes(paths: List[Path]) -> bytes:
63
+ buf = io.BytesIO()
64
+ with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
65
+ for p in paths:
66
+ if p.exists() and p.is_file():
67
+ z.write(p, arcname=p.name)
68
+ return buf.getvalue()
69
+
70
+
71
+ _require_login()
72
+
73
+ make_pdf = True
74
+
75
+ work_root = Path("/tmp/bdc_app")
76
+ work_root.mkdir(parents=True, exist_ok=True)
77
+
78
+
79
+ def _ensure_tmp() -> Path:
80
+ if work_root.exists():
81
+ shutil.rmtree(work_root)
82
+ work_root.mkdir(parents=True, exist_ok=True)
83
+ return work_root
84
+
85
+
86
+ def _load_people_lists(
87
+ c_auto_p: Optional[Path],
88
+ c_val_p: Optional[Path],
89
+ m_auto_p: Optional[Path],
90
+ m_val_p: Optional[Path],
91
+ ) -> Tuple[List[str], List[str]]:
92
+ collab_people: List[str] = []
93
+ manager_people: List[str] = []
94
+ if c_auto_p and c_val_p:
95
+ collab_people = list_people(c_auto_p, c_val_p)
96
+ if m_auto_p and m_val_p:
97
+ manager_people = list_people(m_auto_p, m_val_p)
98
+ return collab_people, manager_people
99
+
100
+
101
+ # ── Sidebar ────────────────────────────────────────────────────────────────────
102
+ with st.sidebar:
103
+ st.title("βš™οΈ Impostazioni")
104
+ sezioni = st.multiselect(
105
+ "Tipologia",
106
+ options=["Collaboratori", "Manager"],
107
+ default=["Collaboratori", "Manager"],
108
+ )
109
+
110
+ show_collab = "Collaboratori" in sezioni
111
+ show_manager = "Manager" in sezioni
112
+
113
+ if show_collab:
114
+ st.divider()
115
+ st.title("πŸ‘₯ Collaboratori")
116
+ tpl_collab = st.file_uploader("Modello Documento", type=["docx"], key="tpl_c")
117
+ c_auto = st.file_uploader("Autovalutazione", type=["xlsx"], key="c_auto")
118
+ c_val = st.file_uploader("Valutazione", type=["xlsx"], key="c_val")
119
+ if all([tpl_collab, c_auto, c_val]):
120
+ st.success("Files collaboratori inseriti", icon="βœ…")
121
+ else:
122
+ st.error("Inserire files collaboratori", icon="❌")
123
+ else:
124
+ tpl_collab = c_auto = c_val = None
125
+
126
+ if show_manager:
127
+ st.divider()
128
+ st.title("🏒 Manager")
129
+ tpl_manager = st.file_uploader("Modello Documento", type=["docx"], key="tpl_m")
130
+ m_auto = st.file_uploader("Autovalutazione", type=["xlsx"], key="m_auto")
131
+ m_val = st.file_uploader("Valutazione", type=["xlsx"], key="m_val")
132
+ if all([tpl_manager, m_auto, m_val]):
133
+ st.success("Files manager inseriti", icon="βœ…")
134
+ else:
135
+ st.error("Inserire files manager", icon="❌")
136
+ else:
137
+ tpl_manager = m_auto = m_val = None
138
+
139
+ collab_ready = all([tpl_collab, c_auto, c_val])
140
+ manager_ready = all([tpl_manager, m_auto, m_val])
141
+ can_prepare = collab_ready or manager_ready
142
+
143
+ st.divider()
144
+ if st.button("Genera Report", type="primary", disabled=not can_prepare, use_container_width=True):
145
+ tmp = _ensure_tmp()
146
+ tpl_c_p = _save_upload(tpl_collab, tmp) if collab_ready else None
147
+ tpl_m_p = _save_upload(tpl_manager, tmp) if manager_ready else None
148
+ c_auto_p = _save_upload(c_auto, tmp) if collab_ready else None
149
+ c_val_p = _save_upload(c_val, tmp) if collab_ready else None
150
+ m_auto_p = _save_upload(m_auto, tmp) if manager_ready else None
151
+ m_val_p = _save_upload(m_val, tmp) if manager_ready else None
152
+
153
+ collab_people, manager_people = _load_people_lists(c_auto_p, c_val_p, m_auto_p, m_val_p)
154
+
155
+ if not collab_people and not manager_people:
156
+ st.error("Non ho trovato nessun nome nei file. Controlla la colonna 'Nome e cognome'.")
157
+ else:
158
+ st.session_state._paths = {
159
+ "tpl_c": str(tpl_c_p) if tpl_c_p else "",
160
+ "tpl_m": str(tpl_m_p) if tpl_m_p else "",
161
+ "c_auto": str(c_auto_p) if c_auto_p else "",
162
+ "c_val": str(c_val_p) if c_val_p else "",
163
+ "m_auto": str(m_auto_p) if m_auto_p else "",
164
+ "m_val": str(m_val_p) if m_val_p else "",
165
+ "tmp": str(tmp),
166
+ }
167
+ st.session_state._collab_people = collab_people
168
+ st.session_state._manager_people = manager_people
169
+ st.session_state._phase = "select"
170
+ st.session_state._results_ready = False
171
+ st.rerun()
172
+
173
+
174
+ # ── Main area ──────────────────────────────────────────────────────────────────
175
+ st.title("πŸ“Š Bilancio Competenze")
176
+ with st.expander("ℹ️ Funzionamento"):
177
+ st.markdown(
178
+ """
179
+ Questa app genera report individuali di **Bilancio delle Competenze** in Word e PDF.
180
+ ##### Istruzioni:
181
+ 1. Nella barra laterale seleziona le sezioni da attivare (**Collaboratori**, **Manager**, o entrambe)
182
+ 2. Carica il template Word e i due file Excel (autovalutazione e valutazione) per ciascuna sezione
183
+ 3. Clicca **Genera Report** β€” scegli le persone da includere e scarica i report
184
+ """
185
+ )
186
+ st.divider()
187
+ phase = st.session_state.get("_phase")
188
+
189
+
190
+ # ── Fase: selezione persone ────────────────────────────────────────────────────
191
+ if phase == "select":
192
+ collab_people: List[str] = st.session_state.get("_collab_people", [])
193
+ manager_people: List[str] = st.session_state.get("_manager_people", [])
194
+
195
+ col_c, col_m = st.columns(2)
196
+ sel_c: List[str] = []
197
+ sel_m: List[str] = []
198
+
199
+ with col_c:
200
+ st.subheader(f"πŸ‘₯ Collaboratori")
201
+ for n in collab_people:
202
+ if st.checkbox(n, value=True, key=f"c_{n}"):
203
+ sel_c.append(n)
204
+
205
+ with col_m:
206
+ st.subheader(f"🏒 Manager")
207
+ for n in manager_people:
208
+ if st.checkbox(n, value=True, key=f"m_{n}"):
209
+ sel_m.append(n)
210
+
211
+ tot = len(sel_c) + len(sel_m)
212
+ c1, c2= st.columns([1, 1])
213
+ with c1:
214
+ if st.button("Annulla", use_container_width=True):
215
+ st.session_state._phase = None
216
+ st.rerun()
217
+ with c2:
218
+ if st.button(f"Genera {tot} report", type="primary", disabled=tot == 0, use_container_width=True):
219
+ st.session_state._selected_c = sel_c
220
+ st.session_state._selected_m = sel_m
221
+ st.session_state._phase = "generate"
222
+ st.rerun()
223
+
224
+
225
+ # ── Fase: generazione ─────────────────────────────────────────────────────────
226
+ elif phase == "generate":
227
+ sel_c = st.session_state.get("_selected_c", [])
228
+ sel_m = st.session_state.get("_selected_m", [])
229
+ total = len(sel_c) + len(sel_m)
230
+
231
+ paths = st.session_state._paths
232
+
233
+ def _p(key: str) -> Optional[Path]:
234
+ v = paths.get(key, "")
235
+ return Path(v) if v else None
236
+
237
+ tpl_c_p, tpl_m_p = _p("tpl_c"), _p("tpl_m")
238
+ c_auto_p, c_val_p = _p("c_auto"), _p("c_val")
239
+ m_auto_p, m_val_p = _p("m_auto"), _p("m_val")
240
+ out_dir = Path(paths["tmp"]) / "output"
241
+ out_dir.mkdir(parents=True, exist_ok=True)
242
+
243
+ header = st.empty()
244
+ header.subheader(f"Generazione in corso… (0/{total})")
245
+ progress = st.progress(0)
246
+ log_area = st.empty()
247
+
248
+ all_produced = []
249
+ all_warnings: List[str] = []
250
+ done = 0
251
+ log_lines: List[str] = []
252
+
253
+ for name in sel_c:
254
+ log_lines.append(f"⏳ Collaboratore: **{name}**")
255
+ log_area.markdown("\n\n".join(log_lines))
256
+ r = generate_selected(
257
+ collab_auto=c_auto_p, collab_valut=c_val_p, collab_template=tpl_c_p,
258
+ manager_auto=None, manager_valut=None, manager_template=None,
259
+ selected_collaboratori=[name], selected_manager=[],
260
+ output_dir=out_dir, make_pdf=make_pdf,
261
+ )
262
+ all_produced.extend(r.produced)
263
+ all_warnings.extend(r.warnings)
264
+ done += 1
265
+ log_lines[-1] = f"βœ… Collaboratore: **{name}**"
266
+ log_area.markdown("\n\n".join(log_lines))
267
+ progress.progress(done / total)
268
+ header.subheader(f"Generazione in corso… ({done}/{total})")
269
+
270
+ for name in sel_m:
271
+ log_lines.append(f"⏳ Manager: **{name}**")
272
+ log_area.markdown("\n\n".join(log_lines))
273
+ r = generate_selected(
274
+ collab_auto=None, collab_valut=None, collab_template=None,
275
+ manager_auto=m_auto_p, manager_valut=m_val_p, manager_template=tpl_m_p,
276
+ selected_collaboratori=[], selected_manager=[name],
277
+ output_dir=out_dir, make_pdf=make_pdf,
278
+ )
279
+ all_produced.extend(r.produced)
280
+ all_warnings.extend(r.warnings)
281
+ done += 1
282
+ log_lines[-1] = f"βœ… Manager: **{name}**"
283
+ log_area.markdown("\n\n".join(log_lines))
284
+ progress.progress(done / total)
285
+ header.subheader(f"Generazione in corso… ({done}/{total})")
286
+
287
+ header.subheader(f"Completato β€” {total} report generati βœ…")
288
+
289
+ st.session_state._results = [
290
+ {"person": a.person, "kind": a.kind,
291
+ "docx_path": a.docx_path, "pdf_path": a.pdf_path, "notes": a.notes}
292
+ for a in all_produced
293
+ ]
294
+ st.session_state._result_warnings = all_warnings
295
+ st.session_state._results_ready = True
296
+ st.session_state._phase = "results"
297
+ st.rerun()
298
+
299
+
300
+ # ── Fase: risultati ────────────────────────────────────────────────────────────
301
+ elif phase == "results":
302
+ results = st.session_state._results
303
+ warnings = st.session_state.get("_result_warnings", [])
304
+
305
+ if warnings:
306
+ st.warning("\n".join(warnings))
307
+
308
+ st.success(f"Generazione completata: {len(results)} report", icon="βœ…")
309
+
310
+ all_files: List[Path] = []
311
+ by_kind: Dict[str, list] = {}
312
+ for a in sorted(results, key=lambda x: (x["kind"], x["person"])):
313
+ by_kind.setdefault(a["kind"], []).append(a)
314
+
315
+ def _render_table(artifacts: list) -> None:
316
+ # Intestazione
317
+ h0, h1, h2 = st.columns([4, 1, 1])
318
+ h0.markdown("**Persona**")
319
+ h1.markdown("**Word**")
320
+ h2.markdown("**PDF**")
321
+ for a in artifacts:
322
+ c0, c1, c2 = st.columns([4, 1, 1])
323
+ label = a["person"]
324
+ if a["notes"]:
325
+ label += f" \n*{a['notes']}*"
326
+ c0.markdown(label)
327
+
328
+ docx_path = Path(a["docx_path"])
329
+ all_files.append(docx_path)
330
+ c1.download_button(
331
+ "DOCX",
332
+ data=docx_path.read_bytes(),
333
+ file_name=docx_path.name,
334
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
335
+ use_container_width=True,
336
+ key=f"docx_{a['kind']}_{a['person']}",
337
+ )
338
+
339
+ has_pdf = a["pdf_path"] and Path(a["pdf_path"]).exists()
340
+ if has_pdf:
341
+ pdf_path = Path(a["pdf_path"])
342
+ all_files.append(pdf_path)
343
+ c2.download_button(
344
+ "PDF",
345
+ data=pdf_path.read_bytes(),
346
+ file_name=pdf_path.name,
347
+ mime="application/pdf",
348
+ use_container_width=True,
349
+ key=f"pdf_{a['kind']}_{a['person']}",
350
+ type="primary",
351
+ )
352
+
353
+ kinds = list(by_kind.keys())
354
+ if len(kinds) > 1:
355
+ tabs = st.tabs(kinds)
356
+ for tab, kind in zip(tabs, kinds):
357
+ with tab:
358
+ _render_table(by_kind[kind])
359
+ else:
360
+ st.subheader(kinds[0])
361
+ _render_table(by_kind[kinds[0]])
362
+
363
+ st.divider()
364
+ c1, c2= st.columns([1, 1])
365
+ with c1:
366
+ if st.button("Annulla", use_container_width=True):
367
+ st.session_state._phase = None
368
+ st.rerun()
369
+ with c2:
370
+ st.download_button(
371
+ "Scarica Tutti i Files",
372
+ data=_zip_bytes(all_files),
373
+ file_name="BDC_report_output.zip",
374
+ mime="application/zip",
375
+ use_container_width=True,
376
+ key="zip_all",
377
+ type="primary",
378
+ )