init
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
# Environment / runtime config
|
||||
.env
|
||||
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# SQLite database files
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
src/services/Reports/Reports.py
|
||||
|
||||
Web server che crea reports HTML live del processo RPA:
|
||||
"""
|
||||
|
||||
__version__ = '1.0.0'
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
# ==============================================================
|
||||
current = Path(__file__).resolve().parent
|
||||
while current != current.parent:
|
||||
if (current / "src").exists():
|
||||
src_dir = current / "src"
|
||||
break
|
||||
current = current.parent
|
||||
else:
|
||||
raise FileNotFoundError("'src' folder not found in parent directories")
|
||||
|
||||
if str(src_dir) not in sys.path:
|
||||
sys.path.insert(0, str(src_dir))
|
||||
# ==============================================================
|
||||
|
||||
REPORTS_DIR = Path(__file__).resolve().parent
|
||||
TMPL_DIR = REPORTS_DIR / "templates"
|
||||
CSS_PATH = REPORTS_DIR / "static" / "style.css"
|
||||
PAGES_CSS_DIR = REPORTS_DIR / "static" / "pages"
|
||||
JS_DIR = REPORTS_DIR / "static" / "js"
|
||||
|
||||
DB_DEFAULT_DIR = src_dir.parent / "data_rpa" / "db" # <root>/data_rpa/db
|
||||
|
||||
# buffer circolare degli accessi HTTP (max 200 righe)
|
||||
_access_log: deque = deque(maxlen=200)
|
||||
|
||||
# stato condiviso (db_path aggiornabile via /change-db)
|
||||
_state: dict = {'db_path': '', 'step': 'unknown'}
|
||||
|
||||
|
||||
def _refresh_step(db_path: str) -> str:
|
||||
"""Detect and cache the RPA step for the current DB."""
|
||||
step = _detect_step(db_path)
|
||||
_state['step'] = step
|
||||
return step
|
||||
|
||||
import json
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
from queries import processes as q_processes
|
||||
from queries import pec as q_pec
|
||||
from queries import logs as q_logs
|
||||
from queries import report as q_report
|
||||
from queries import documenti as q_documenti
|
||||
from queries import iscrizioni as q_iscrizioni
|
||||
from queries.db import e as _e, query as _query, detect_step as _detect_step
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ templates
|
||||
|
||||
def _css() -> str:
|
||||
return CSS_PATH.read_text(encoding='utf-8')
|
||||
|
||||
def _page_css(name: str) -> str:
|
||||
parts = []
|
||||
shared = PAGES_CSS_DIR / 'filters.css'
|
||||
if shared.exists():
|
||||
parts.append(shared.read_text(encoding='utf-8'))
|
||||
main = PAGES_CSS_DIR / f'{name}.css'
|
||||
if main.exists():
|
||||
parts.append(main.read_text(encoding='utf-8'))
|
||||
for f in sorted(PAGES_CSS_DIR.glob(f'{name}_*.css')):
|
||||
parts.append(f.read_text(encoding='utf-8'))
|
||||
return '\n'.join(parts)
|
||||
|
||||
def _page_js(name: str) -> str:
|
||||
parts = []
|
||||
shared = JS_DIR / 'filters.js'
|
||||
if shared.exists():
|
||||
parts.append(shared.read_text(encoding='utf-8'))
|
||||
main = JS_DIR / f'{name}.js'
|
||||
if main.exists():
|
||||
parts.append(main.read_text(encoding='utf-8'))
|
||||
for f in sorted(JS_DIR.glob(f'{name}_*.js')):
|
||||
parts.append(f.read_text(encoding='utf-8'))
|
||||
return '\n'.join(parts)
|
||||
|
||||
def _tmpl(name: str) -> Template:
|
||||
return Template((TMPL_DIR / name).read_text(encoding='utf-8'))
|
||||
|
||||
def _base(title: str, content: str, active: str, db_path: str, page_css: str = '', page_js: str = '') -> str:
|
||||
now = datetime.now().strftime('%d/%m/%Y %H:%M:%S')
|
||||
nav = {'nav_dashboard': '', 'nav_runs': '', 'nav_logs': '', 'nav_report': ''}
|
||||
if active in nav:
|
||||
nav[active] = 'active'
|
||||
|
||||
step = _state.get('step', 'unknown')
|
||||
|
||||
h1_title = 'RPA — Corsi Intraziendali' if step == 'step2' else 'RPA — Comunicazioni Regione Lombardia'
|
||||
|
||||
# dynamic nav items depend on step
|
||||
if step == 'step2':
|
||||
cls = 'active' if active == 'nav_iscrizioni' else ''
|
||||
nav_step_link = f'<a href="/iscrizioni" class="{cls}">Corsi Intraziendali</a>'
|
||||
nav_pec_link = ''
|
||||
nav_report_link = ''
|
||||
else:
|
||||
cls = 'active' if active == 'nav_documenti' else ''
|
||||
nav_step_link = f'<a href="/documenti" class="{cls}">Documenti</a>'
|
||||
cls_pec = 'active' if active == 'nav_pec' else ''
|
||||
nav_pec_link = f'<a href="/pec" class="{cls_pec}">PEC</a>'
|
||||
cls_rep = nav.get('nav_report', '')
|
||||
nav_report_link = f'<a href="/report" class="{cls_rep}">Report</a>'
|
||||
|
||||
return _tmpl('_base.html').substitute(
|
||||
css=_css(), page_css=page_css, page_js=page_js, title=title, now=now,
|
||||
db_name=_e(Path(db_path).name),
|
||||
h1_title=h1_title,
|
||||
content=content,
|
||||
nav_step_link=nav_step_link,
|
||||
nav_pec_link=nav_pec_link,
|
||||
nav_report_link=nav_report_link,
|
||||
**nav,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ DB picker
|
||||
|
||||
def _pick_db_file() -> str:
|
||||
"""Apre una finestra di dialogo per scegliere il file .db."""
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes('-topmost', True)
|
||||
path = filedialog.askopenfilename(
|
||||
title="Seleziona il file database RPA",
|
||||
filetypes=[("SQLite Database", "*.db"), ("Tutti i file", "*.*")]
|
||||
)
|
||||
root.destroy()
|
||||
return path or ''
|
||||
except Exception as e:
|
||||
print(f"Impossibile aprire il dialogo file: {e}")
|
||||
return ''
|
||||
|
||||
|
||||
def _find_db(db_file: str) -> str:
|
||||
"""Cerca il DB in DB_DEFAULT_DIR; se non trovato apre popup."""
|
||||
candidate = DB_DEFAULT_DIR / db_file
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
print(f"DB '{db_file}' non trovato in {DB_DEFAULT_DIR}. Apertura dialogo selezione file...")
|
||||
chosen = _pick_db_file()
|
||||
if chosen and os.path.exists(chosen):
|
||||
return chosen
|
||||
|
||||
raise FileNotFoundError(
|
||||
f"DB '{db_file}' non trovato in {DB_DEFAULT_DIR}. "
|
||||
"Imposta RPA_DB_PATH con il percorso assoluto al file .db"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ DB helpers
|
||||
|
||||
def _load_data(db_path: str) -> dict:
|
||||
step = _state.get('step', 'unknown')
|
||||
middle_stats = (
|
||||
q_iscrizioni.get_stats(db_path) if step == 'step2'
|
||||
else q_pec.get_stats(db_path)
|
||||
)
|
||||
stats = {
|
||||
**q_processes.get_stats(db_path),
|
||||
**middle_stats,
|
||||
**q_logs.get_stats(db_path),
|
||||
}
|
||||
return {
|
||||
'processes': q_processes.get_processes(db_path),
|
||||
'pec': [] if step == 'step2' else q_pec.get_pec(db_path),
|
||||
'logs': q_logs.get_logs(db_path),
|
||||
'stats': stats,
|
||||
}
|
||||
|
||||
|
||||
def _card(label, value, color='#2563eb') -> str:
|
||||
return f'<div class="card"><div class="cv" style="color:{color}">{_e(value)}</div><div class="cl">{_e(label)}</div></div>'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ page builders
|
||||
|
||||
def _page_dashboard(db_path: str) -> str:
|
||||
s = _load_data(db_path)['stats']
|
||||
step = _state.get('step', 'unknown')
|
||||
|
||||
cards_runs = (
|
||||
_card("Run totali", s['total_runs'])
|
||||
+ _card("Completati", s['completed_runs'], '#16a34a')
|
||||
+ _card("Incompleti", s['total_runs'] - s['completed_runs'], '#dc2626')
|
||||
)
|
||||
cards_logs = (
|
||||
_card("Errori", s['log_errors'], '#dc2626')
|
||||
+ _card("Warning", s['log_warnings'], '#d97706')
|
||||
)
|
||||
|
||||
if step == 'step2':
|
||||
cards_middle = (
|
||||
_card("Corsi totali", s.get('isc_total', 0))
|
||||
+ _card("Completati", s.get('isc_ok', 0), '#16a34a')
|
||||
+ _card("Con errore", s.get('isc_errore', 0), '#dc2626')
|
||||
+ _card("In corso", s.get('isc_pending', 0), '#d97706')
|
||||
)
|
||||
section_middle = (
|
||||
'<section>'
|
||||
'<h2><a href="/iscrizioni" class="sec-link">📋 Corsi Intraziendali</a></h2>'
|
||||
f'<div class="grid">{cards_middle}</div>'
|
||||
'</section>'
|
||||
)
|
||||
else:
|
||||
cards_middle = (
|
||||
_card("PEC totali", s.get('total_pec', 0))
|
||||
+ _card("Inviate", s.get('pec_inviato', 0), '#16a34a')
|
||||
+ _card("Pending", s.get('pec_pending', 0), '#d97706')
|
||||
+ _card("Errori", s.get('pec_errore', 0), '#dc2626')
|
||||
)
|
||||
section_middle = (
|
||||
'<section>'
|
||||
'<h2><a href="/pec" class="sec-link">✉ Comunicazioni PEC</a></h2>'
|
||||
f'<div class="grid">{cards_middle}</div>'
|
||||
'</section>'
|
||||
)
|
||||
|
||||
content = _tmpl('dashboard.html').substitute(
|
||||
cards_runs=cards_runs, section_middle=section_middle, cards_logs=cards_logs
|
||||
)
|
||||
return _base('Dashboard', content, 'nav_dashboard', db_path, _page_css('dashboard'), _page_js('dashboard'))
|
||||
|
||||
|
||||
def _page_runs(db_path: str) -> str:
|
||||
content = _tmpl('runs.html').substitute(
|
||||
tbl_processes=q_processes.render_table(q_processes.get_processes(db_path))
|
||||
)
|
||||
return _base('Processi', content, 'nav_runs', db_path, _page_css('runs'), _page_js('runs'))
|
||||
|
||||
|
||||
def _page_pec(db_path: str) -> str:
|
||||
content = _tmpl('pec.html').substitute(
|
||||
tbl_pec=q_pec.render_table(q_pec.get_pec(db_path))
|
||||
)
|
||||
return _base('PEC', content, 'nav_pec', db_path, _page_css('pec'), _page_js('pec'))
|
||||
|
||||
|
||||
def _page_logs(db_path: str, run_id: int = None) -> str:
|
||||
run_ids = [r['rpa_process_id'] for r in q_logs.get_last_run_ids(db_path)]
|
||||
|
||||
if not run_ids:
|
||||
content = _tmpl('logs.html').substitute(
|
||||
run_info='nessun log', pagination='', tbl_logs='<p class="empty">Nessun log trovato.</p>'
|
||||
)
|
||||
return _base('Log DB', content, 'nav_logs', db_path, _page_css('logs'), _page_js('logs'))
|
||||
|
||||
if run_id is None or run_id not in run_ids:
|
||||
run_id = run_ids[0]
|
||||
|
||||
idx = run_ids.index(run_id)
|
||||
prev_id = run_ids[idx + 1] if idx + 1 < len(run_ids) else None
|
||||
next_id = run_ids[idx - 1] if idx > 0 else None
|
||||
|
||||
prev_btn = (f'<a class="pg-btn" href="/logs?run={prev_id}">← Precedente</a>'
|
||||
if prev_id else '<span class="pg-btn disabled">← Precedente</span>')
|
||||
next_btn = (f'<a class="pg-btn" href="/logs?run={next_id}">Successivo →</a>'
|
||||
if next_id else '<span class="pg-btn disabled">Successivo →</span>')
|
||||
options = ''.join(
|
||||
f'<option value="{rid}"{" selected" if rid == run_id else ""}>Run #{rid}</option>'
|
||||
for rid in run_ids
|
||||
)
|
||||
pagination = (
|
||||
f'<div class="log-pagination">'
|
||||
f'{prev_btn}'
|
||||
f'<select class="pg-select" onchange="location.href=\'/logs?run=\'+this.value">{options}</select>'
|
||||
f'{next_btn}'
|
||||
f'<span class="pg-info">Run {idx + 1} / {len(run_ids)}</span>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
logs = q_logs.get_logs_by_run(db_path, run_id)
|
||||
content = _tmpl('logs.html').substitute(
|
||||
run_info=f'Run #{run_id}',
|
||||
pagination=pagination,
|
||||
tbl_logs=q_logs.render_table(logs),
|
||||
)
|
||||
return _base('Log DB', content, 'nav_logs', db_path, _page_css('logs'), _page_js('logs'))
|
||||
|
||||
|
||||
def _page_report(db_path: str) -> str:
|
||||
rows = q_report.get_report(db_path)
|
||||
content = _tmpl('report.html').substitute(
|
||||
tbl_report=q_report.render_table(rows)
|
||||
)
|
||||
return _base('Report', content, 'nav_report', db_path, _page_css('report'), _page_js('report'))
|
||||
|
||||
|
||||
def _page_documenti(db_path: str) -> str:
|
||||
rows = q_documenti.get_documenti(db_path)
|
||||
content = _tmpl('documenti.html').substitute(
|
||||
sessions_html=q_documenti.render_page(rows)
|
||||
)
|
||||
return _base('Documenti', content, 'nav_documenti', db_path, _page_css('documenti'), _page_js('documenti'))
|
||||
|
||||
|
||||
def _page_iscrizioni(db_path: str) -> str:
|
||||
rows = q_iscrizioni.get_corsi(db_path)
|
||||
content = _tmpl('iscrizioni.html').substitute(
|
||||
tbl_iscrizioni=q_iscrizioni.render_table(rows),
|
||||
filter_stato_html=q_iscrizioni.render_stato_filters(),
|
||||
)
|
||||
return _base('Corsi Intraziendali', content, 'nav_iscrizioni', db_path, _page_css('iscrizioni'), _page_js('iscrizioni'))
|
||||
|
||||
|
||||
def _page_server_logs(db_path: str) -> str:
|
||||
rows = ''.join(
|
||||
f'<tr>'
|
||||
f'<td>{_e(e["ts"])}</td><td>{_e(e["method"])}</td><td>{_e(e["path"])}</td>'
|
||||
f'<td style="color:{"#16a34a" if e["code"]<400 else "#d97706" if e["code"]<500 else "#dc2626"};font-weight:600">{e["code"]}</td>'
|
||||
f'<td>{_e(e["client"])}</td>'
|
||||
f'</tr>'
|
||||
for e in reversed(_access_log)
|
||||
)
|
||||
table = (
|
||||
'<table><thead><tr>'
|
||||
'<th>Timestamp</th><th>Method</th><th>Path</th><th>Status</th><th>Client</th>'
|
||||
'</tr></thead><tbody>'
|
||||
+ (rows or '<tr><td colspan=5 class="empty">Nessuna richiesta ancora.</td></tr>')
|
||||
+ '</tbody></table>'
|
||||
)
|
||||
content = f'<section><h2>Accessi HTTP (ultimi 200)</h2><div class="tw">{table}</div></section>'
|
||||
return _base('Server Logs', content, '', db_path, _page_css('server-logs'))
|
||||
|
||||
|
||||
def make_handler(db_path: str):
|
||||
class ReportHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {fmt % args}")
|
||||
|
||||
def _send(self, code: int, body: str, content_type: str = 'text/html; charset=utf-8'):
|
||||
encoded = body.encode('utf-8')
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.send_header('Content-Length', str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
_access_log.append({
|
||||
'ts': datetime.now().strftime('%d/%m/%Y %H:%M:%S'),
|
||||
'method': self.command,
|
||||
'path': self.path,
|
||||
'code': code,
|
||||
'client': self.client_address[0],
|
||||
})
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/change-db':
|
||||
chosen = _pick_db_file()
|
||||
if chosen and os.path.exists(chosen):
|
||||
_state['db_path'] = chosen
|
||||
_refresh_step(chosen)
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/')
|
||||
self.end_headers()
|
||||
_access_log.append({
|
||||
'ts': datetime.now().strftime('%d/%m/%Y %H:%M:%S'),
|
||||
'method': self.command, 'path': self.path,
|
||||
'code': 302, 'client': self.client_address[0],
|
||||
})
|
||||
return
|
||||
|
||||
db_path = _state['db_path']
|
||||
|
||||
step = _state.get('step', 'unknown')
|
||||
|
||||
# JSON API
|
||||
if self.path == '/api/documenti' and step != 'step2':
|
||||
try:
|
||||
rows = q_documenti.get_documenti(db_path)
|
||||
html = q_documenti.render_page(rows)
|
||||
ts = datetime.now().strftime('%d/%m/%Y %H:%M:%S')
|
||||
self._send(200, json.dumps({'html': html, 'ts': ts}), 'application/json; charset=utf-8')
|
||||
except Exception as exc:
|
||||
self._send(500, json.dumps({'error': str(exc)}), 'application/json; charset=utf-8')
|
||||
return
|
||||
|
||||
if self.path.startswith('/api/docs') and step != 'step2':
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
sid = qs.get('sessione_id', [None])[0]
|
||||
if sid:
|
||||
try:
|
||||
rows = _query(db_path,
|
||||
"SELECT subfolder, filename, doc_status, sp_remote_path, sp_web_url, uploaded_at "
|
||||
"FROM sessione_documenti WHERE sessione_id=? ORDER BY subfolder, filename",
|
||||
(int(sid),)
|
||||
)
|
||||
self._send(200, json.dumps(rows), 'application/json; charset=utf-8')
|
||||
except Exception as exc:
|
||||
self._send(500, json.dumps({'error': str(exc)}), 'application/json; charset=utf-8')
|
||||
else:
|
||||
self._send(400, json.dumps({'error': 'missing sessione_id'}), 'application/json; charset=utf-8')
|
||||
return
|
||||
|
||||
if self.path == '/api/iscrizioni' and step == 'step2':
|
||||
try:
|
||||
rows = q_iscrizioni.get_iscrizioni(db_path)
|
||||
ts = datetime.now().strftime('%d/%m/%Y %H:%M:%S')
|
||||
self._send(200, json.dumps({'rows': rows, 'ts': ts}), 'application/json; charset=utf-8')
|
||||
except Exception as exc:
|
||||
self._send(500, json.dumps({'error': str(exc)}), 'application/json; charset=utf-8')
|
||||
return
|
||||
|
||||
parsed = urlparse(self.path)
|
||||
qs = parse_qs(parsed.query)
|
||||
path = parsed.path
|
||||
|
||||
def _logs_page():
|
||||
raw = qs.get('run', [None])[0]
|
||||
run_id = int(raw) if raw and raw.isdigit() else None
|
||||
return _page_logs(db_path, run_id=run_id)
|
||||
|
||||
routes = {
|
||||
'/': lambda: _page_dashboard(db_path),
|
||||
'/runs': lambda: _page_runs(db_path),
|
||||
'/logs': _logs_page,
|
||||
'/server-logs': lambda: _page_server_logs(db_path),
|
||||
}
|
||||
if step == 'step2':
|
||||
routes['/iscrizioni'] = lambda: _page_iscrizioni(db_path)
|
||||
else:
|
||||
routes['/report'] = lambda: _page_report(db_path)
|
||||
routes['/documenti'] = lambda: _page_documenti(db_path)
|
||||
routes['/pec'] = lambda: _page_pec(db_path)
|
||||
fn = routes.get(path)
|
||||
if fn:
|
||||
try:
|
||||
self._send(200, fn())
|
||||
except Exception as exc:
|
||||
self._send(500, f"<pre>Errore: {_e(str(exc))}</pre>")
|
||||
elif path == '/ping':
|
||||
self._send(200, 'ok', 'text/plain; charset=utf-8')
|
||||
else:
|
||||
self._send(404, '<h1>404 Not Found</h1>')
|
||||
|
||||
return ReportHandler
|
||||
|
||||
|
||||
def run_server(db_path: str, host: str = 'localhost', port: int = 8080):
|
||||
_state['db_path'] = db_path
|
||||
step = _refresh_step(db_path)
|
||||
handler = make_handler(db_path)
|
||||
server = HTTPServer((host, port), handler)
|
||||
print(f"RPA Report server avviato [step: {step}]")
|
||||
print(f" Dashboard : http://localhost:{port}/")
|
||||
print(f" Processi : http://localhost:{port}/runs")
|
||||
if step == 'step2':
|
||||
print(f" Iscrizioni : http://localhost:{port}/iscrizioni")
|
||||
else:
|
||||
print(f" PEC : http://localhost:{port}/pec")
|
||||
print(f" Documenti : http://localhost:{port}/documenti")
|
||||
print(f" Log DB : http://localhost:{port}/logs")
|
||||
print(f" Report : http://localhost:{port}/report")
|
||||
print(f" Server log : http://localhost:{port}/server-logs")
|
||||
print(f" DB : {db_path}")
|
||||
print("Ctrl+C per fermare.\n")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer fermato.")
|
||||
server.server_close()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ entrypoint
|
||||
|
||||
if __name__ == '__main__':
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
db_path = os.environ.get('RPA_DB_PATH')
|
||||
if not db_path:
|
||||
db_file = os.environ.get('RPA_DB_FILE', 'rpa_FORMAZIONE.db')
|
||||
db_path = _find_db(db_file)
|
||||
|
||||
port = int(os.environ.get('RPA_REPORT_PORT', 8080))
|
||||
run_server(db_path, port=port)
|
||||
@@ -0,0 +1,14 @@
|
||||
Web server che serve un report HTML live del processo RPA:
|
||||
- GET / -> pagina report (dati freschi dal DB ad ogni richiesta)
|
||||
- GET /ping -> healthcheck
|
||||
|
||||
Avvio:
|
||||
```bash
|
||||
cd src
|
||||
python rpa/Reports/Reports.py
|
||||
```
|
||||
|
||||
Opzionalmente via env:
|
||||
```bash
|
||||
RPA_DB_FILE=rpa_FORMAZIONE.db RPA_REPORT_PORT=8080 python rpa/Reports/Reports.py
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
"""queries/db.py — helper condiviso per query SQLite"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def query(db_path: str, sql: str, params: tuple = ()) -> list:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, params)
|
||||
return [dict(r) for r in cur.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def count(db_path: str, sql: str) -> int:
|
||||
rows = query(db_path, sql)
|
||||
return rows[0]['n'] if rows else 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ HTML utils
|
||||
|
||||
def e(v) -> str:
|
||||
if v is None: return '-'
|
||||
return str(v).replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
def dt(v) -> str:
|
||||
if not v: return '-'
|
||||
try:
|
||||
return datetime.fromisoformat(str(v)).strftime('%d/%m/%Y %H:%M:%S')
|
||||
except Exception:
|
||||
return str(v)
|
||||
|
||||
def d(v) -> str:
|
||||
if not v: return '-'
|
||||
try:
|
||||
return datetime.fromisoformat(str(v)).strftime('%d/%m/%Y')
|
||||
except Exception:
|
||||
return str(v)
|
||||
|
||||
def dur(start, finish) -> str:
|
||||
if not start or not finish: return '-'
|
||||
try:
|
||||
s = int((datetime.fromisoformat(str(finish)) - datetime.fromisoformat(str(start))).total_seconds())
|
||||
return f"{s//60}m {s%60}s" if s >= 60 else f"{s}s"
|
||||
except Exception:
|
||||
return '-'
|
||||
|
||||
def badge(cls: str, text: str) -> str:
|
||||
return f'<span class="badge {cls}">{text}</span>'
|
||||
|
||||
|
||||
def detect_step(db_path: str) -> str:
|
||||
"""Returns 'step1', 'step2', or 'unknown' based on DB schema.
|
||||
|
||||
step1 (db_reg_lombardia) → has table 'sessione_documenti'
|
||||
step2 (db_corsi_intraziendali) → has table 'rpa_intra_ftp_json'
|
||||
"""
|
||||
rows = query(db_path, "SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = {r['name'] for r in rows}
|
||||
if 'sessione_documenti' in tables:
|
||||
return 'step1'
|
||||
if 'rpa_intra_api_iscrizione' in tables:
|
||||
return 'step2'
|
||||
return 'unknown'
|
||||
@@ -0,0 +1,126 @@
|
||||
"""queries/documenti.py — sessione_documenti con join sessione"""
|
||||
|
||||
from .db import query, e, dt
|
||||
|
||||
|
||||
_STATUS_BADGE = {
|
||||
'trovato': '<span class="badge doc-trovato">trovato</span>',
|
||||
'caricato': '<span class="badge doc-caricato">caricato</span>',
|
||||
'errore': '<span class="badge doc-errore">errore</span>',
|
||||
'warning': '<span class="badge doc-warning">warning</span>',
|
||||
}
|
||||
|
||||
|
||||
def get_documenti(db_path: str) -> list:
|
||||
sql = """
|
||||
SELECT
|
||||
sd.sessione_id,
|
||||
s.corso,
|
||||
s.azienda,
|
||||
sd.subfolder,
|
||||
sd.doc_type,
|
||||
sd.doc_status,
|
||||
sd.filename,
|
||||
sd.note,
|
||||
sd.sp_web_url,
|
||||
sd.uploaded_at
|
||||
FROM sessione_documenti sd
|
||||
LEFT JOIN sessione s ON s.id = sd.sessione_id
|
||||
ORDER BY sd.sessione_id, sd.subfolder, sd.filename
|
||||
"""
|
||||
return query(db_path, sql)
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
rows = get_documenti(db_path)
|
||||
return {
|
||||
'doc_trovato': sum(1 for r in rows if r['doc_status'] == 'trovato'),
|
||||
'doc_caricato': sum(1 for r in rows if r['doc_status'] == 'caricato'),
|
||||
'doc_errore': sum(1 for r in rows if r['doc_status'] == 'errore'),
|
||||
'doc_sessioni': len({r['sessione_id'] for r in rows}),
|
||||
}
|
||||
|
||||
|
||||
def render_page(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty-msg">Nessun documento registrato.</p>'
|
||||
|
||||
# Group by sessione_id
|
||||
sessions: dict = {}
|
||||
order: list = []
|
||||
for r in rows:
|
||||
sid = r['sessione_id']
|
||||
if sid not in sessions:
|
||||
sessions[sid] = {'corso': r['corso'], 'azienda': r['azienda'], 'docs': []}
|
||||
order.append(sid)
|
||||
sessions[sid]['docs'].append(r)
|
||||
|
||||
html_parts = []
|
||||
for sid in order:
|
||||
s = sessions[sid]
|
||||
docs = s['docs']
|
||||
n_trovato = sum(1 for d in docs if d['doc_status'] == 'trovato')
|
||||
n_caricato = sum(1 for d in docs if d['doc_status'] == 'caricato')
|
||||
n_errore = sum(1 for d in docs if d['doc_status'] == 'errore')
|
||||
n_warning = sum(1 for d in docs if d['doc_status'] == 'warning')
|
||||
|
||||
counts_html = (
|
||||
(f'<span class="cnt cnt-caricato">{n_caricato} caricati</span>' if n_caricato else '')
|
||||
+ (f'<span class="cnt cnt-trovato">{n_trovato} trovati</span>' if n_trovato else '')
|
||||
+ (f'<span class="cnt cnt-warning">{n_warning} warning</span>' if n_warning else '')
|
||||
+ (f'<span class="cnt cnt-errore">{n_errore} errori</span>' if n_errore else '')
|
||||
)
|
||||
|
||||
tbody_rows = ''
|
||||
for d in docs:
|
||||
status = d['doc_status'] or ''
|
||||
badge_html = _STATUS_BADGE.get(status, f'<span class="badge">{e(status)}</span>')
|
||||
if d['sp_web_url']:
|
||||
sp_cell = f'<a href="{e(d["sp_web_url"])}" target="_blank" rel="noopener" class="sp-link" title="Apri su SharePoint">🔗</a>'
|
||||
else:
|
||||
sp_cell = '<span class="sp-missing" title="Non caricato su SharePoint">❌</span>'
|
||||
tbody_rows += (
|
||||
f'<tr class="doc-row doc-{e(status)}"'
|
||||
f' data-filename="{e(d["filename"] or "")}"'
|
||||
f' data-doctype="{e(d["doc_type"] or "")}"'
|
||||
f' data-subfolder="{e(d["subfolder"] or "")}"'
|
||||
f' data-status="{e(status)}"'
|
||||
f' data-note="{e(d["note"] or "")}"'
|
||||
f' data-uploaded="{e(dt(d["uploaded_at"]) if status == "caricato" else "")}">'
|
||||
f'<td class="col-filename" title="{e(d["filename"] or "")}">{"<span style=\'color:#f97316;\'>" + e(d["filename"]) + "</span>" if d["filename"] and d["filename"].startswith("[") and d["filename"].endswith("]") else e(d["filename"])}</td>'
|
||||
f'<td class="col-doctype">{e(d["doc_type"])}</td>'
|
||||
f'<td class="col-subfolder">{e(d["subfolder"])}</td>'
|
||||
f'<td class="col-status">{badge_html}</td>'
|
||||
f'<td class="col-sp">{sp_cell}</td>'
|
||||
f'<td class="col-note">{e(d["note"])}</td>'
|
||||
f'<td class="col-uploaded">{dt(d["uploaded_at"]) if status == "caricato" else "-"}</td>'
|
||||
f'</tr>'
|
||||
)
|
||||
|
||||
subtable = (
|
||||
'<table class="doc-subtable">'
|
||||
'<thead><tr>'
|
||||
'<th>File</th><th>Tipo</th><th>Cartella</th>'
|
||||
'<th>Stato</th><th>SP</th><th>Note</th><th>Caricato</th>'
|
||||
'</tr></thead>'
|
||||
f'<tbody>{tbody_rows}</tbody>'
|
||||
'</table>'
|
||||
)
|
||||
|
||||
open_attr = ' open' if (n_errore > 0 or n_warning > 0) else ''
|
||||
html_parts.append(
|
||||
f'<details class="sess-block"{open_attr}'
|
||||
f' data-sessione="{e(sid)}"'
|
||||
f' data-corso="{e(s["corso"])}"'
|
||||
f' data-azienda="{e(s["azienda"])}">'
|
||||
f'<summary class="sess-summary">'
|
||||
f'<span class="sess-id">#{e(sid)}</span>'
|
||||
f'<span class="sess-corso">{e(s["corso"])}</span>'
|
||||
f'<span class="sess-azienda">{e(s["azienda"])}</span>'
|
||||
f'<span class="sess-counts">{counts_html}</span>'
|
||||
f'</summary>'
|
||||
f'<div class="sess-docs">{subtable}</div>'
|
||||
f'</details>'
|
||||
)
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
@@ -0,0 +1,121 @@
|
||||
"""queries/iscrizioni.py — corsi intraziendali (step2: rpa_intra_ftp_json + rpa_intra_steps_status)"""
|
||||
|
||||
from .db import query, e, dt, badge
|
||||
from rpa_intraziendale.shared.SqlLite.tables import STEP_STATES, STEP_STATE_LABELS
|
||||
|
||||
|
||||
_STEP_BADGE = {
|
||||
'done': lambda: badge('ok', 'done'),
|
||||
'processing': lambda: badge('proc', 'processing'),
|
||||
'pending': lambda: badge('warn', 'pending'),
|
||||
'error': lambda: badge('err', 'error'),
|
||||
'skipped': lambda: badge('info', 'skipped'),
|
||||
}
|
||||
|
||||
def _sb(v) -> str:
|
||||
fn = _STEP_BADGE.get(v)
|
||||
return fn() if fn else (badge('warn', 'pending') if not v else e(v))
|
||||
|
||||
|
||||
def get_corsi(db_path: str) -> list:
|
||||
return query(db_path, """
|
||||
SELECT
|
||||
f.id,
|
||||
f.rpa_process_id,
|
||||
f.company_code,
|
||||
m.ragione_sociale AS sorgente,
|
||||
json_extract(f.azienda, '$.ragione_sociale') AS azienda_nome,
|
||||
json_extract(f.corso, '$.IDsessione') AS sessione_id,
|
||||
json_extract(f.corso, '$.descrizione') AS corso_descrizione,
|
||||
json_extract(f.corso, '$.quando_start') AS corso_data,
|
||||
json_extract(f.corso, '$.n_iscritti') AS n_iscritti,
|
||||
s.step_ftp,
|
||||
s.step_bc,
|
||||
s.step_iscrizione,
|
||||
s.step_odv,
|
||||
s.step_sharepoint,
|
||||
s.step_report,
|
||||
s.step_email,
|
||||
s.error_note,
|
||||
f.created_at
|
||||
FROM rpa_intra_ftp_json f
|
||||
LEFT JOIN rpa_mapping_aziende m ON m.company_code = f.company_code
|
||||
LEFT JOIN rpa_intra_steps_status s ON s.rpa_intra_ftp_json_id = f.id
|
||||
ORDER BY f.id DESC
|
||||
""")
|
||||
|
||||
|
||||
_ALL_STEPS = ('step_ftp', 'step_bc', 'step_iscrizione', 'step_odv',
|
||||
'step_sharepoint', 'step_report', 'step_email')
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
rows = get_corsi(db_path)
|
||||
return {
|
||||
'isc_total': len(rows),
|
||||
'isc_ok': sum(1 for r in rows if all(r.get(s) in ('done', 'skipped') for s in _ALL_STEPS)),
|
||||
'isc_errore': sum(1 for r in rows if any(r.get(s) == 'error' for s in _ALL_STEPS)),
|
||||
'isc_pending': sum(1 for r in rows if _row_status(r) in ('pending', 'processing', 'skipped')),
|
||||
}
|
||||
|
||||
|
||||
def _row_status(r: dict) -> str:
|
||||
if any(r.get(s) == 'error' for s in _ALL_STEPS):
|
||||
return 'error'
|
||||
if all(r.get(s) in ('done', 'skipped') for s in _ALL_STEPS):
|
||||
return 'done'
|
||||
if any(r.get(s) == 'processing' for s in _ALL_STEPS):
|
||||
return 'processing'
|
||||
if any(r.get(s) == 'skipped' for s in _ALL_STEPS):
|
||||
return 'skipped'
|
||||
return 'pending'
|
||||
|
||||
|
||||
def render_stato_filters() -> str:
|
||||
return ''.join(
|
||||
f'<label><input type="checkbox" data-status="{s}" checked> {e(STEP_STATE_LABELS.get(s, s))}</label>'
|
||||
for s in STEP_STATES
|
||||
)
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty">Nessun corso trovato.</p>'
|
||||
|
||||
trs = ''.join(
|
||||
f'<tr'
|
||||
f' data-status="{_row_status(r)}"'
|
||||
f' data-sorgente="{e(r.get("sorgente"))}"'
|
||||
f' data-sessione="{e(r.get("sessione_id"))}">'
|
||||
f'<td>{e(r.get("id"))}</td>'
|
||||
f'<td>{e(r.get("sorgente"))}</td>'
|
||||
f'<td>{e(r.get("azienda_nome"))}</td>'
|
||||
f'<td>{e(r.get("sessione_id"))}</td>'
|
||||
f'<td class="nc">{e(r.get("corso_descrizione"))}</td>'
|
||||
f'<td>{e(r.get("corso_data"))}</td>'
|
||||
f'<td>{e(r.get("n_iscritti"))}</td>'
|
||||
f'<td>{_sb(r.get("step_ftp"))}</td>'
|
||||
f'<td>{_sb(r.get("step_bc"))}</td>'
|
||||
f'<td>{_sb(r.get("step_iscrizione"))}</td>'
|
||||
f'<td>{_sb(r.get("step_odv"))}</td>'
|
||||
f'<td>{_sb(r.get("step_sharepoint"))}</td>'
|
||||
f'<td>{_sb(r.get("step_report"))}</td>'
|
||||
f'<td>{_sb(r.get("step_email"))}</td>'
|
||||
f'<td class="nc">{e(r.get("error_note"))}</td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
'<th>#</th><th>Sorgente</th><th>Azienda</th><th>Sessione</th>'
|
||||
'<th>Corso</th><th>Data</th><th>N.</th>'
|
||||
'<th>FTP</th><th>BC</th><th>Iscrizione</th><th>ODV</th>'
|
||||
'<th>SharePoint</th><th>Report</th><th>Email</th>'
|
||||
'<th>Note</th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
|
||||
|
||||
# backward-compat alias used by Reports.py
|
||||
def get_iscrizioni(db_path: str) -> list:
|
||||
return get_corsi(db_path)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""queries/logs.py — rpa_logs"""
|
||||
|
||||
from .db import query, count, e, dt, badge
|
||||
|
||||
|
||||
def get_logs(db_path: str) -> list:
|
||||
return query(db_path, "SELECT * FROM rpa_logs ORDER BY date_log DESC LIMIT 200")
|
||||
|
||||
|
||||
def get_last_run_ids(db_path: str, n: int = 10) -> list:
|
||||
"""Returns the last N distinct rpa_process_id values that have logs, DESC."""
|
||||
return query(db_path,
|
||||
"SELECT rpa_process_id FROM rpa_logs WHERE rpa_process_id IS NOT NULL "
|
||||
"GROUP BY rpa_process_id ORDER BY rpa_process_id DESC LIMIT ?", (n,))
|
||||
|
||||
|
||||
def get_logs_by_run(db_path: str, run_id: int) -> list:
|
||||
return query(db_path,
|
||||
"SELECT * FROM rpa_logs WHERE rpa_process_id=? ORDER BY date_log DESC", (run_id,))
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
return {
|
||||
'log_errors': count(db_path, "SELECT COUNT(*) as n FROM rpa_logs WHERE type='error'"),
|
||||
'log_warnings': count(db_path, "SELECT COUNT(*) as n FROM rpa_logs WHERE type='warning'"),
|
||||
}
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty">Nessun log trovato.</p>'
|
||||
|
||||
def _badge(tipo):
|
||||
m = {'log': ('log', 'Log'), 'warning': ('warn', 'Warning'), 'error': ('err', 'Error')}
|
||||
cls, lbl = m.get(tipo, ('', ''))
|
||||
return badge(cls, lbl) if cls else e(tipo)
|
||||
|
||||
trs = ''.join(
|
||||
f'<tr data-type="{e(r.get("type", ""))}">'
|
||||
f'<td>{e(r.get("rpa_process_id"))}</td>'
|
||||
f'<td>{dt(r.get("date_log"))}</td>'
|
||||
f'<td>{_badge(r.get("type"))}</td>'
|
||||
f'<td class="nc">{e(r.get("log"))}</td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
'<th>Processo</th><th>Data</th><th>Tipo</th><th>Messaggio</th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""queries/pec.py — pec_comunicazioni"""
|
||||
|
||||
from .db import query, count, e, dt, badge
|
||||
|
||||
_PEC_JOIN = """
|
||||
SELECT
|
||||
p.id, p.sessione_id, p.tipo_comunicazione,
|
||||
p.email_ats, p.stato_invio, p.data_invio_pec, p.created_at,
|
||||
p.note,
|
||||
s.articolo_cod, s.corso, s.azienda,
|
||||
sl.data AS data_sessione, sl.indirizzo, sl.docenti
|
||||
FROM pec_comunicazioni p
|
||||
JOIN sessione s ON s.id = p.sessione_id
|
||||
LEFT JOIN sessione_lezione sl ON sl.sessione_id = p.sessione_id
|
||||
"""
|
||||
|
||||
|
||||
def get_pec(db_path: str) -> list:
|
||||
return query(db_path, _PEC_JOIN + "ORDER BY p.created_at DESC LIMIT 200")
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
return {
|
||||
'total_pec': count(db_path, "SELECT COUNT(*) as n FROM pec_comunicazioni"),
|
||||
'pec_inviato': count(db_path, "SELECT COUNT(*) as n FROM pec_comunicazioni WHERE stato_invio='inviata'"),
|
||||
'pec_errore': count(db_path, "SELECT COUNT(*) as n FROM pec_comunicazioni WHERE stato_invio='errore'"),
|
||||
'pec_pending': count(db_path, "SELECT COUNT(*) as n FROM pec_comunicazioni WHERE stato_invio='pending'"),
|
||||
}
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty">Nessuna comunicazione PEC trovata.</p>'
|
||||
|
||||
def _badge(stato):
|
||||
m = {'inviata': ('ok', 'Inviata'), 'pending': ('warn', 'Pending'), 'errore': ('err', 'Errore')}
|
||||
cls, lbl = m.get(stato, ('', ''))
|
||||
return badge(cls, lbl) if cls else e(stato)
|
||||
|
||||
trs = ''.join(
|
||||
f'<tr data-stato="{e(r.get("stato_invio", ""))}">'
|
||||
f'<td>{e(r.get("articolo_cod"))}</td>'
|
||||
f'<td>{e(r.get("sessione_id"))}</td>'
|
||||
f'<td>{dt(r.get("data_sessione"))}</td>'
|
||||
f'<td>{e(r.get("tipo_comunicazione"))}</td>'
|
||||
f'<td>{e(r.get("email_ats"))}</td>'
|
||||
f'<td>{dt(r.get("data_invio_pec"))}</td>'
|
||||
f'<td>{_badge(r.get("stato_invio"))}</td>'
|
||||
f'<td class="nc">{e(r.get("note"))}</td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
'<th>Corso</th><th>Sessione</th><th>Data Sessione</th><th>Tipo</th>'
|
||||
'<th>Email ATS</th><th>Data Invio</th><th>Stato</th><th>Note</th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
"""queries/processes.py — rpa_process"""
|
||||
|
||||
from .db import query, count, e, dt, dur, badge
|
||||
|
||||
|
||||
def get_processes(db_path: str) -> list:
|
||||
return query(db_path, "SELECT * FROM rpa_process ORDER BY start_run DESC LIMIT 50")
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
total = count(db_path, "SELECT COUNT(*) as n FROM rpa_process")
|
||||
completed = count(db_path, "SELECT COUNT(*) as n FROM rpa_process WHERE completed = 1")
|
||||
return {
|
||||
'total_runs': total,
|
||||
'completed_runs': completed,
|
||||
'incomplete_runs': total - completed,
|
||||
}
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty">Nessun processo trovato.</p>'
|
||||
|
||||
def _badge(completed):
|
||||
return badge('ok', 'Completato') if completed in (1, True, '1') else badge('err', 'Incompleto')
|
||||
|
||||
trs = ''.join(
|
||||
f'<tr data-completed="{1 if r.get("completed") in (1, True, "1") else 0}">'
|
||||
f'<td>{dt(r.get("start_run"))}</td>'
|
||||
f'<td>{dt(r.get("finish_run"))}</td>'
|
||||
f'<td>{dur(r.get("start_run"), r.get("finish_run"))}</td>'
|
||||
f'<td>{_badge(r.get("completed"))}</td>'
|
||||
f'<td class="nc">{e(r.get("note"))}</td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
'<th>Inizio</th><th>Fine</th><th>Durata</th><th>Stato</th><th>Note</th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""queries/report.py — sessione_changes con join sessione"""
|
||||
|
||||
from .db import query, e, dt, d, badge
|
||||
|
||||
|
||||
def get_report(db_path: str, da_data: str = None, a_data: str = None) -> list:
|
||||
"""
|
||||
Restituisce sessione_changes JOIN sessione, opzionalmente filtrate per data.
|
||||
Ordinate dalla più recente.
|
||||
"""
|
||||
params = []
|
||||
where = []
|
||||
if da_data:
|
||||
where.append("sc.rilevato_at >= ?")
|
||||
params.append(da_data)
|
||||
if a_data:
|
||||
where.append("sc.rilevato_at <= ?")
|
||||
params.append(a_data + " 23:59:59")
|
||||
|
||||
where_sql = ("WHERE " + " AND ".join(where)) if where else ""
|
||||
|
||||
sql = f"""
|
||||
SELECT
|
||||
sc.id AS change_id,
|
||||
sc.change_type,
|
||||
sc.campo,
|
||||
sc.valore_prec,
|
||||
sc.valore_nuovo,
|
||||
sc.rilevato_at,
|
||||
sc.rpa_process_id,
|
||||
s.id AS sessione_id,
|
||||
s.corso,
|
||||
s.variante,
|
||||
s.modalita_cod,
|
||||
s.modalita_desc,
|
||||
s.monte_ore,
|
||||
s.soggetto_formatore,
|
||||
s.azienda,
|
||||
s.articolo_cod,
|
||||
s.articolo_desc,
|
||||
s.requisiti_formativi_id,
|
||||
s.requisiti_formativi_desc,
|
||||
(SELECT MIN(sl.data) FROM sessione_lezione sl WHERE sl.sessione_id = s.id) AS prima_lezione
|
||||
FROM sessione_changes sc
|
||||
LEFT JOIN sessione s ON s.id = sc.sessione_id
|
||||
{where_sql}
|
||||
ORDER BY sc.rilevato_at DESC
|
||||
"""
|
||||
return query(db_path, sql, tuple(params))
|
||||
|
||||
|
||||
def get_stats(db_path: str, da_data: str = None, a_data: str = None) -> dict:
|
||||
rows = get_report(db_path, da_data, a_data)
|
||||
nuovi = sum(1 for r in rows if r['change_type'] == 'nuovo')
|
||||
cambiati = sum(1 for r in rows if r['change_type'] == 'modificato')
|
||||
return {'report_nuovi': nuovi, 'report_cambiati': cambiati, 'report_total': len(rows)}
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<table><thead><tr><th colspan="11">Nessun dato</th></tr></thead><tbody></tbody></table>'
|
||||
|
||||
def row_html(r):
|
||||
tag = badge('ok', 'Nuovo') if r['change_type'] == 'nuovo' else badge('warn', 'Modificato')
|
||||
campo = e(r['campo']) if r['campo'] else '-'
|
||||
prec = e(r['valore_prec']) if r['valore_prec'] else '-'
|
||||
nuovo = e(r['valore_nuovo']) if r['valore_nuovo'] else '-'
|
||||
sid = e(r['sessione_id'])
|
||||
return (
|
||||
f'<tr data-tipo="{e(r["change_type"])}">'
|
||||
f'<td>{tag}</td>'
|
||||
f'<td>{sid}</td>'
|
||||
f'<td>{e(r["corso"])}</td>'
|
||||
f'<td>{e(r["azienda"])}</td>'
|
||||
f'<td>{e(r["articolo_cod"])}</td>'
|
||||
f'<td class="col-prima-lez">{d(r["prima_lezione"])}</td>'
|
||||
f'<td>{campo}</td>'
|
||||
f'<td class="val-prec">{prec}</td>'
|
||||
f'<td class="val-nuovo">{nuovo}</td>'
|
||||
f'<td>{dt(r["rilevato_at"])}</td>'
|
||||
f'<td><button class="doc-btn" data-sid="{sid}" data-corso="{e(r["corso"])}" title="Documenti caricati">📄</button></td>'
|
||||
f'</tr>'
|
||||
)
|
||||
|
||||
thead = (
|
||||
'<thead><tr>'
|
||||
'<th>Tipo</th>'
|
||||
'<th>ID Sessione</th>'
|
||||
'<th>Corso</th>'
|
||||
'<th>Azienda</th>'
|
||||
'<th>Articolo</th>'
|
||||
'<th>Prima Lezione</th>'
|
||||
'<th>Campo</th>'
|
||||
'<th>Valore Precedente</th>'
|
||||
'<th>Valore Nuovo</th>'
|
||||
'<th>Rilevato</th>'
|
||||
'<th></th>'
|
||||
'</tr></thead>'
|
||||
)
|
||||
tbody = '<tbody>' + ''.join(row_html(r) for r in rows) + '</tbody>'
|
||||
return f'<table>{thead}{tbody}</table>'
|
||||
@@ -0,0 +1,177 @@
|
||||
// ── documenti.js — core namespace, refresh, pagination, settings ──
|
||||
var _Doc = {};
|
||||
_Doc.blocks = [];
|
||||
_Doc.PAGE_SIZE = 10;
|
||||
_Doc.currentPage = 1;
|
||||
_Doc.autorefreshTimer = null;
|
||||
|
||||
_Doc.rebuildBlocks = function() {
|
||||
_Doc.blocks = Array.prototype.slice.call(document.querySelectorAll('.sess-block'));
|
||||
};
|
||||
|
||||
_Doc.resetPage = function() { _Doc.currentPage = 1; };
|
||||
|
||||
_Doc.saveFilterState = function() {
|
||||
var tipo = {}, cartella = {};
|
||||
document.querySelectorAll('.tipo-chk').forEach(function(c){ tipo[c.value] = c.checked; });
|
||||
document.querySelectorAll('.cartella-chk').forEach(function(c){ cartella[c.value] = c.checked; });
|
||||
return { tipo: tipo, cartella: cartella };
|
||||
};
|
||||
|
||||
_Doc.doRefresh = function() {
|
||||
var btn = document.getElementById('btn-refresh');
|
||||
if (btn) { btn.disabled = true; btn.textContent = '\u27F3 Aggiornamento\u2026'; }
|
||||
var state = _Doc.saveFilterState();
|
||||
fetch('/api/documenti')
|
||||
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||
.then(function(data) {
|
||||
document.getElementById('doc-content').innerHTML = data.html;
|
||||
_Doc.rebuildBlocks();
|
||||
_Doc.buildDynamicFilters(state.tipo, state.cartella);
|
||||
_Doc.applyFilters();
|
||||
var tsEl = document.getElementById('doc-refresh-ts');
|
||||
if (tsEl) tsEl.textContent = data.ts;
|
||||
})
|
||||
.catch(function(err) { console.error('Refresh error:', err); })
|
||||
.finally(function() {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '\u21BB Aggiorna dati'; }
|
||||
});
|
||||
};
|
||||
|
||||
// ── Pagination ─────────────────────────────────────────────────────────────
|
||||
// Pagination uses .pag-hidden class (not inline style) so export still works on all filtered data.
|
||||
|
||||
_Doc.applyPagination = function() {
|
||||
// Remove any previous pagination-hiding first
|
||||
_Doc.blocks.forEach(function(b) { b.classList.remove('pag-hidden'); });
|
||||
|
||||
var visible = _Doc.blocks.filter(function(b) { return b.style.display !== 'none'; });
|
||||
var total = Math.max(1, Math.ceil(visible.length / _Doc.PAGE_SIZE));
|
||||
var page = _Doc.currentPage;
|
||||
if (page > total) page = _Doc.currentPage = total;
|
||||
if (page < 1) page = _Doc.currentPage = 1;
|
||||
|
||||
var countEl = document.getElementById('doc-visible-count');
|
||||
|
||||
if (total <= 1) {
|
||||
var pag = document.getElementById('doc-pagination');
|
||||
if (pag) pag.innerHTML = '';
|
||||
if (countEl) countEl.textContent = visible.length + ' sessioni';
|
||||
return;
|
||||
}
|
||||
|
||||
var start = (page - 1) * _Doc.PAGE_SIZE;
|
||||
visible.forEach(function(b, i) {
|
||||
b.classList.toggle('pag-hidden', i < start || i >= start + _Doc.PAGE_SIZE);
|
||||
});
|
||||
|
||||
if (countEl) countEl.textContent = visible.length + ' sessioni \u2014 pag. ' + page + '/' + total;
|
||||
_Doc.renderPagination(page, total);
|
||||
};
|
||||
|
||||
_Doc.renderPagination = function(page, total) {
|
||||
var pag = document.getElementById('doc-pagination');
|
||||
if (!pag) return;
|
||||
|
||||
var lo = Math.max(1, page - 3);
|
||||
var hi = Math.min(total, lo + 6);
|
||||
lo = Math.max(1, hi - 6);
|
||||
|
||||
var html = '<button class="doc-pag-btn" data-page="' + (page - 1) + '"' + (page === 1 ? ' disabled' : '') + '>«</button>';
|
||||
if (lo > 1) {
|
||||
html += '<button class="doc-pag-btn" data-page="1">1</button>';
|
||||
if (lo > 2) html += '<span class="doc-pag-ellipsis">…</span>';
|
||||
}
|
||||
for (var i = lo; i <= hi; i++) {
|
||||
html += '<button class="doc-pag-btn' + (i === page ? ' active' : '') + '" data-page="' + i + '">' + i + '</button>';
|
||||
}
|
||||
if (hi < total) {
|
||||
if (hi < total - 1) html += '<span class="doc-pag-ellipsis">…</span>';
|
||||
html += '<button class="doc-pag-btn" data-page="' + total + '">' + total + '</button>';
|
||||
}
|
||||
html += '<button class="doc-pag-btn" data-page="' + (page + 1) + '"' + (page === total ? ' disabled' : '') + '>»</button>';
|
||||
|
||||
pag.innerHTML = html;
|
||||
pag.querySelectorAll('.doc-pag-btn:not([disabled])').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
_Doc.currentPage = parseInt(this.getAttribute('data-page'), 10);
|
||||
_Doc.applyPagination();
|
||||
document.getElementById('doc-content').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ── Event listeners ────────────────────────────────────────────────────────
|
||||
document.getElementById('btn-refresh').addEventListener('click', _Doc.doRefresh);
|
||||
|
||||
document.getElementById('btn-expand-all').addEventListener('click', function() {
|
||||
_Doc.blocks.forEach(function(b) {
|
||||
if (b.style.display !== 'none' && !b.classList.contains('pag-hidden')) b.open = true;
|
||||
});
|
||||
});
|
||||
document.getElementById('btn-collapse-all').addEventListener('click', function() {
|
||||
_Doc.blocks.forEach(function(b) {
|
||||
if (!b.classList.contains('pag-hidden')) b.open = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Settings modal ─────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
var wrenchDrop = document.querySelector('.wrench-drop');
|
||||
if (wrenchDrop) {
|
||||
var link = document.createElement('a');
|
||||
link.id = 'btn-page-settings';
|
||||
link.href = '#';
|
||||
link.innerHTML = '⚙ Impostazioni…';
|
||||
wrenchDrop.appendChild(link);
|
||||
}
|
||||
|
||||
var modal = document.createElement('div');
|
||||
modal.id = 'page-settings-modal';
|
||||
modal.className = 'page-settings-overlay';
|
||||
modal.innerHTML =
|
||||
'<div class="page-settings-dialog">' +
|
||||
'<div class="page-settings-hdr">' +
|
||||
'<span>Impostazioni pagina</span>' +
|
||||
'<button class="page-settings-close" id="btn-settings-close" title="Chiudi">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="page-settings-body">' +
|
||||
'<div class="page-settings-row"><label><input type="checkbox" id="autorefresh-toggle"> Auto-aggiornamento</label></div>' +
|
||||
'<div class="page-settings-row"><span>Intervallo (sec)</span><input type="number" id="autorefresh-seconds" class="page-settings-input" value="60" min="5" max="3600"></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(modal);
|
||||
|
||||
function openModal() { modal.classList.add('open'); }
|
||||
function closeModal() { modal.classList.remove('open'); }
|
||||
|
||||
var settingsBtn = document.getElementById('btn-page-settings');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
var drop = document.querySelector('.wrench-drop');
|
||||
if (drop) drop.classList.remove('open');
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
document.getElementById('btn-settings-close').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); });
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });
|
||||
|
||||
document.getElementById('autorefresh-toggle').addEventListener('change', function() {
|
||||
if (this.checked) { startAutorefresh(); } else { stopAutorefresh(); }
|
||||
});
|
||||
document.getElementById('autorefresh-seconds').addEventListener('change', function() {
|
||||
if (document.getElementById('autorefresh-toggle').checked) { startAutorefresh(); }
|
||||
});
|
||||
})();
|
||||
|
||||
function startAutorefresh() {
|
||||
stopAutorefresh();
|
||||
var secs = parseInt(document.getElementById('autorefresh-seconds').value, 10) || 60;
|
||||
if (secs < 5) secs = 5;
|
||||
_Doc.autorefreshTimer = setInterval(_Doc.doRefresh, secs * 1000);
|
||||
}
|
||||
function stopAutorefresh() {
|
||||
if (_Doc.autorefreshTimer) { clearInterval(_Doc.autorefreshTimer); _Doc.autorefreshTimer = null; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// ── documenti_export.js — Excel & PDF export ──
|
||||
// Depends on _Doc namespace. Export uses ALL filter-visible blocks (not just current page).
|
||||
|
||||
document.getElementById('btn-excel').addEventListener('click', function() {
|
||||
var csvRows = [['Sessione', 'Corso', 'Azienda', 'Cartella', 'Tipo', 'Stato', 'File', 'Note', 'Caricato']];
|
||||
_Doc.blocks.forEach(function(block) {
|
||||
if (block.style.display === 'none') return; // filter-hidden: skip
|
||||
var sessione = block.dataset.sessione || '';
|
||||
var corso = block.dataset.corso || '';
|
||||
var azienda = block.dataset.azienda || '';
|
||||
block.querySelectorAll('.doc-row').forEach(function(row) {
|
||||
if (row.style.display === 'none') return;
|
||||
csvRows.push([
|
||||
sessione, corso, azienda,
|
||||
row.dataset.subfolder || '',
|
||||
row.dataset.doctype || '',
|
||||
row.dataset.status || '',
|
||||
row.dataset.filename || '',
|
||||
row.dataset.note || '',
|
||||
row.dataset.uploaded || '',
|
||||
]);
|
||||
});
|
||||
});
|
||||
var csv = csvRows.map(function(r) {
|
||||
return r.map(function(v) {
|
||||
var s = String(v).replace(/"/g, '""');
|
||||
return (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1) ? '"' + s + '"' : s;
|
||||
}).join(',');
|
||||
}).join('\r\n');
|
||||
var blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'documenti_' + new Date().toISOString().slice(0, 10) + '.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
document.getElementById('btn-pdf').addEventListener('click', function() {
|
||||
// Temporarily show all filter-visible blocks (including pag-hidden) for print
|
||||
var pagHidden = _Doc.blocks.filter(function(b) { return b.classList.contains('pag-hidden'); });
|
||||
pagHidden.forEach(function(b) { b.classList.remove('pag-hidden'); });
|
||||
|
||||
var wasOpen = _Doc.blocks.map(function(b) { return b.open; });
|
||||
_Doc.blocks.forEach(function(b) {
|
||||
if (b.style.display === 'none') {
|
||||
b.classList.add('print-hidden');
|
||||
} else {
|
||||
var visibleRows = Array.prototype.filter.call(b.querySelectorAll('.doc-row'), function(r) { return r.style.display !== 'none'; });
|
||||
if (visibleRows.length === 0) { b.classList.add('print-hidden'); } else { b.open = true; }
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.doc-row').forEach(function(r) {
|
||||
if (r.style.display === 'none') r.classList.add('print-hidden');
|
||||
});
|
||||
|
||||
window.print();
|
||||
|
||||
// Restore state
|
||||
_Doc.blocks.forEach(function(b, i) { b.classList.remove('print-hidden'); b.open = wasOpen[i]; });
|
||||
document.querySelectorAll('.doc-row.print-hidden').forEach(function(r) { r.classList.remove('print-hidden'); });
|
||||
pagHidden.forEach(function(b) { b.classList.add('pag-hidden'); });
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
// ── documenti_filters.js — filter logic, event listeners, init ──
|
||||
// Depends on _Doc namespace from documenti.js (loaded first).
|
||||
|
||||
function _buildCheckboxGroup(containerId, cssClass, values, prevState) {
|
||||
var container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
values.forEach(function(val) {
|
||||
var lbl = document.createElement('label');
|
||||
lbl.className = 'chk-label';
|
||||
var chk = document.createElement('input');
|
||||
chk.type = 'checkbox';
|
||||
chk.className = cssClass;
|
||||
chk.value = val;
|
||||
chk.checked = prevState ? (prevState[val] !== false) : true;
|
||||
lbl.appendChild(chk);
|
||||
lbl.appendChild(document.createTextNode(' ' + val));
|
||||
container.appendChild(lbl);
|
||||
});
|
||||
}
|
||||
|
||||
function _uniqueValues(attr) {
|
||||
var seen = {}, result = [];
|
||||
document.querySelectorAll('.doc-row[data-' + attr + ']').forEach(function(r) {
|
||||
var v = r.dataset[attr];
|
||||
if (v && !seen[v]) { seen[v] = true; result.push(v); }
|
||||
});
|
||||
result.sort();
|
||||
return result;
|
||||
}
|
||||
|
||||
_Doc.buildDynamicFilters = function(tipoState, cartellaState) {
|
||||
_buildCheckboxGroup('filter-tipo', 'tipo-chk', _uniqueValues('doctype'), tipoState);
|
||||
_buildCheckboxGroup('filter-cartella', 'cartella-chk', _uniqueValues('subfolder'), cartellaState);
|
||||
};
|
||||
|
||||
_Doc.applyFilters = function() {
|
||||
var q = document.getElementById('doc-search').value.toLowerCase();
|
||||
|
||||
var checkedStatus = [];
|
||||
document.querySelectorAll('.status-chk:checked').forEach(function(c){ checkedStatus.push(c.value); });
|
||||
var checkedTipo = [];
|
||||
document.querySelectorAll('.tipo-chk:checked').forEach(function(c){ checkedTipo.push(c.value); });
|
||||
var checkedCartella = [];
|
||||
document.querySelectorAll('.cartella-chk:checked').forEach(function(c){ checkedCartella.push(c.value); });
|
||||
|
||||
_Doc.blocks.forEach(function(block) {
|
||||
var text = (block.dataset.sessione + ' ' + block.dataset.corso + ' ' + block.dataset.azienda).toLowerCase();
|
||||
var matchQ = !q || text.indexOf(q) !== -1;
|
||||
var rows = Array.prototype.slice.call(block.querySelectorAll('.doc-row'));
|
||||
var hasVisible = rows.length === 0;
|
||||
rows.forEach(function(row) {
|
||||
var status = row.dataset.status || '';
|
||||
var doctype = row.dataset.doctype || '';
|
||||
var subfolder = row.dataset.subfolder || '';
|
||||
var show = checkedStatus.indexOf(status) !== -1
|
||||
&& (!doctype || checkedTipo.length === 0 || checkedTipo.indexOf(doctype) !== -1)
|
||||
&& (!subfolder || checkedCartella.length === 0 || checkedCartella.indexOf(subfolder) !== -1);
|
||||
row.style.display = show ? '' : 'none';
|
||||
if (show) hasVisible = true;
|
||||
});
|
||||
block.style.display = (matchQ && hasVisible) ? '' : 'none';
|
||||
});
|
||||
|
||||
_Doc.resetPage();
|
||||
_Doc.applyPagination();
|
||||
};
|
||||
|
||||
// ── Event listeners ────────────────────────────────────────────────────────
|
||||
document.getElementById('doc-search').addEventListener('input', _Doc.applyFilters);
|
||||
document.querySelectorAll('.status-chk').forEach(function(c) {
|
||||
c.addEventListener('change', _Doc.applyFilters);
|
||||
});
|
||||
document.getElementById('filter-tipo').addEventListener('change', _Doc.applyFilters);
|
||||
document.getElementById('filter-cartella').addEventListener('change', _Doc.applyFilters);
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────────────
|
||||
_Doc.rebuildBlocks();
|
||||
_Doc.buildDynamicFilters(null, null);
|
||||
_Doc.applyFilters();
|
||||
@@ -0,0 +1,12 @@
|
||||
// ── filters.js — shared filter toggle (all pages) ──
|
||||
(function () {
|
||||
var btn = document.getElementById('btn-toggle-filters');
|
||||
var collapsible = document.getElementById('filters-collapsible');
|
||||
var arrow = document.getElementById('filters-arrow');
|
||||
if (!btn || !collapsible) return;
|
||||
btn.addEventListener('click', function () {
|
||||
var closing = !collapsible.classList.contains('hidden');
|
||||
collapsible.classList.toggle('hidden', closing);
|
||||
if (arrow) arrow.classList.toggle('collapsed', closing);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,59 @@
|
||||
(function () {
|
||||
// Populate sorgente checkboxes dynamically from table data
|
||||
var sorgSet = [];
|
||||
var seen = {};
|
||||
document.querySelectorAll('tbody tr[data-sorgente]').forEach(function (tr) {
|
||||
var v = tr.getAttribute('data-sorgente');
|
||||
if (v && v !== '-' && !seen[v]) { seen[v] = true; sorgSet.push(v); }
|
||||
});
|
||||
|
||||
var sorgWrap = document.getElementById('filter-sorgente');
|
||||
sorgSet.forEach(function (s) {
|
||||
var lbl = document.createElement('label');
|
||||
var chk = document.createElement('input');
|
||||
chk.type = 'checkbox';
|
||||
chk.setAttribute('data-sorgente', s);
|
||||
chk.checked = true;
|
||||
lbl.appendChild(chk);
|
||||
lbl.appendChild(document.createTextNode(' ' + s));
|
||||
sorgWrap.appendChild(lbl);
|
||||
});
|
||||
if (!sorgSet.length) {
|
||||
var wrap = document.getElementById('filter-sorgente-wrap');
|
||||
if (wrap) wrap.style.display = 'none';
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
var search = (document.getElementById('isc-search').value || '').trim().toLowerCase();
|
||||
|
||||
var activeStatus = {};
|
||||
document.querySelectorAll('#filter-stato input[type="checkbox"]').forEach(function (chk) {
|
||||
activeStatus[chk.getAttribute('data-status')] = chk.checked;
|
||||
});
|
||||
|
||||
var activeSorg = null;
|
||||
if (sorgSet.length) {
|
||||
activeSorg = {};
|
||||
document.querySelectorAll('#filter-sorgente input[type="checkbox"]').forEach(function (chk) {
|
||||
activeSorg[chk.getAttribute('data-sorgente')] = chk.checked;
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('tbody tr[data-status]').forEach(function (tr) {
|
||||
var status = tr.getAttribute('data-status') || 'pending';
|
||||
var sorg = tr.getAttribute('data-sorgente') || '-';
|
||||
|
||||
var show = activeStatus[status] !== false;
|
||||
if (activeSorg) show = show && activeSorg[sorg] !== false;
|
||||
if (show && search) show = tr.textContent.toLowerCase().indexOf(search) !== -1;
|
||||
|
||||
tr.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('isc-search').addEventListener('input', applyFilter);
|
||||
document.querySelectorAll('#filter-stato input[type="checkbox"]').forEach(function (chk) {
|
||||
chk.addEventListener('change', applyFilter);
|
||||
});
|
||||
if (sorgWrap) sorgWrap.addEventListener('change', applyFilter);
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
(function(){
|
||||
var search = document.getElementById('log-search');
|
||||
var checks = document.querySelectorAll('.type-filter');
|
||||
function filter(){
|
||||
var q = search.value.toLowerCase();
|
||||
var active = {};
|
||||
checks.forEach(function(c){ if(c.checked) active[c.value] = 1; });
|
||||
document.querySelectorAll('tbody tr').forEach(function(tr){
|
||||
var type = tr.dataset.type || '';
|
||||
var text = tr.textContent.toLowerCase();
|
||||
tr.style.display = (active[type] && text.indexOf(q) >= 0) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
search.addEventListener('input', filter);
|
||||
checks.forEach(function(c){ c.addEventListener('change', filter); });
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
(function(){
|
||||
function applyFilter() {
|
||||
var active = {};
|
||||
document.querySelectorAll('.pec-filter input[type="checkbox"]').forEach(function(chk) {
|
||||
active[chk.getAttribute('data-stato')] = chk.checked;
|
||||
});
|
||||
document.querySelectorAll('tbody tr[data-stato]').forEach(function(tr) {
|
||||
var stato = tr.getAttribute('data-stato');
|
||||
var show = (stato in active) ? active[stato] : true;
|
||||
tr.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.pec-filter input[type="checkbox"]').forEach(function(chk) {
|
||||
chk.addEventListener('change', applyFilter);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,233 @@
|
||||
/* ── Filter + grouped view ─────────────────────────────────────────────── */
|
||||
(function(){
|
||||
var today = new Date().toISOString().slice(0,10);
|
||||
var sevenAgo = new Date(Date.now() - 7*86400000).toISOString().slice(0,10);
|
||||
document.getElementById('f-da').value = sevenAgo;
|
||||
document.getElementById('f-a').value = today;
|
||||
|
||||
var table = document.querySelector('.tw table');
|
||||
var tbody = table.querySelector('tbody');
|
||||
var thead = table.querySelector('thead');
|
||||
|
||||
var allFlatRows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
|
||||
var flatTheadHTML = thead.innerHTML;
|
||||
var groupedTheadHTML = '<tr>'
|
||||
+ '<th>Tipo</th><th>ID Sessione</th><th>Corso</th>'
|
||||
+ '<th>Azienda</th><th>Articolo</th><th>Prima Lezione</th><th>Modifiche</th><th>Rilevato</th><th></th>'
|
||||
+ '</tr>';
|
||||
|
||||
var isGrouped = false;
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
function badgeHtml(tipo) {
|
||||
if (tipo === 'nuovo') return '<span class="badge ok">Nuovo</span>';
|
||||
if (tipo === 'modificato') return '<span class="badge warn">Modificato</span>';
|
||||
return esc(tipo);
|
||||
}
|
||||
|
||||
function getRowData(row) {
|
||||
var c = row.cells;
|
||||
return {
|
||||
tipo: row.getAttribute('data-tipo') || '',
|
||||
sessione_id: c[1] ? c[1].textContent.trim() : '',
|
||||
corso: c[2] ? c[2].textContent.trim() : '',
|
||||
azienda: c[3] ? c[3].textContent.trim() : '',
|
||||
articolo: c[4] ? c[4].textContent.trim() : '',
|
||||
prima_lezione: c[5] ? c[5].textContent.trim() : '',
|
||||
campo: c[6] ? c[6].textContent.trim() : '',
|
||||
valore_prec: c[7] ? c[7].textContent.trim() : '',
|
||||
valore_nuovo: c[8] ? c[8].textContent.trim() : '',
|
||||
rilevato: c[9] ? c[9].textContent.trim() : '',
|
||||
};
|
||||
}
|
||||
|
||||
function toIso(rilevato) {
|
||||
var m = rilevato.match(/(\d{2})\/(\d{2})\/(\d{4})/);
|
||||
return m ? m[3] + '-' + m[2] + '-' + m[1] : '';
|
||||
}
|
||||
|
||||
function getFilters() {
|
||||
var tipi = [];
|
||||
document.querySelectorAll('.tipo-chk:checked').forEach(function(c){ tipi.push(c.value); });
|
||||
return {
|
||||
da: document.getElementById('f-da').value,
|
||||
a: document.getElementById('f-a').value,
|
||||
sid: document.getElementById('f-sid').value.trim(),
|
||||
tipi: tipi,
|
||||
};
|
||||
}
|
||||
|
||||
function passes(d, f) {
|
||||
var pd = toIso(d.prima_lezione);
|
||||
return (!f.da || pd >= f.da)
|
||||
&& (!f.a || pd <= f.a)
|
||||
&& (!f.sid || d.sessione_id === f.sid)
|
||||
&& f.tipi.indexOf(d.tipo) !== -1;
|
||||
}
|
||||
|
||||
function updateCount(n) {
|
||||
document.getElementById('visible-count').textContent = n + ' righe';
|
||||
}
|
||||
|
||||
function applyFlat(f) {
|
||||
tbody.innerHTML = '';
|
||||
allFlatRows.forEach(function(r) { tbody.appendChild(r); });
|
||||
var n = 0;
|
||||
allFlatRows.forEach(function(row) {
|
||||
var ok = passes(getRowData(row), f);
|
||||
row.style.display = ok ? '' : 'none';
|
||||
if (ok) n++;
|
||||
});
|
||||
updateCount(n);
|
||||
}
|
||||
|
||||
function applyGrouped(f) {
|
||||
var visible = allFlatRows.filter(function(r) { return passes(getRowData(r), f); });
|
||||
|
||||
var groups = {}, order = [];
|
||||
visible.forEach(function(row) {
|
||||
var d = getRowData(row);
|
||||
var key = d.sessione_id || '__?__';
|
||||
if (!groups[key]) {
|
||||
groups[key] = { base: d, tipi: [], changes: [], latestIso: '', latestRilevato: '' };
|
||||
order.push(key);
|
||||
}
|
||||
var g = groups[key];
|
||||
if (g.tipi.indexOf(d.tipo) === -1) g.tipi.push(d.tipo);
|
||||
var iso = toIso(d.rilevato);
|
||||
if (iso > g.latestIso) { g.latestIso = iso; g.latestRilevato = d.rilevato; }
|
||||
if (d.campo && d.campo !== '-') {
|
||||
g.changes.push({ campo: d.campo, prec: d.valore_prec, nuovo: d.valore_nuovo });
|
||||
}
|
||||
});
|
||||
|
||||
var html = '';
|
||||
order.forEach(function(key) {
|
||||
var g = groups[key];
|
||||
var b = g.base;
|
||||
var tipiHtml = g.tipi.map(badgeHtml).join(' ');
|
||||
var modHtml = g.changes.length
|
||||
? g.changes.map(function(c) {
|
||||
return '<div class="mod-row">'
|
||||
+ '<span class="mod-campo">' + esc(c.campo) + '</span>: '
|
||||
+ '<span class="val-prec">' + esc(c.prec) + '</span>'
|
||||
+ ' → '
|
||||
+ '<span class="val-nuovo">' + esc(c.nuovo) + '</span>'
|
||||
+ '</div>';
|
||||
}).join('')
|
||||
: '<span class="mod-none">\u2014</span>';
|
||||
|
||||
html += '<tr>'
|
||||
+ '<td>' + tipiHtml + '</td>'
|
||||
+ '<td>' + esc(b.sessione_id) + '</td>'
|
||||
+ '<td>' + esc(b.corso) + '</td>'
|
||||
+ '<td>' + esc(b.azienda) + '</td>'
|
||||
+ '<td>' + esc(b.articolo) + '</td>'
|
||||
+ '<td class="col-prima-lez">' + esc(b.prima_lezione) + '</td>'
|
||||
+ '<td class="mod-cell">' + modHtml + '</td>'
|
||||
+ '<td>' + esc(g.latestRilevato) + '</td>'
|
||||
+ '<td><button class="doc-btn" data-sid="' + esc(b.sessione_id) + '" data-corso="' + esc(b.corso) + '" title="Documenti caricati">📄</button></td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
|
||||
tbody.innerHTML = html || '<tr><td colspan="9" class="empty">Nessun dato</td></tr>';
|
||||
updateCount(order.length);
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
var f = getFilters();
|
||||
if (isGrouped) applyGrouped(f); else applyFlat(f);
|
||||
}
|
||||
|
||||
document.getElementById('btn-toggle-view').addEventListener('click', function() {
|
||||
isGrouped = !isGrouped;
|
||||
this.textContent = isGrouped ? '\u229e Tabella' : '\u2a27 Raggruppa corso';
|
||||
this.classList.toggle('active', isGrouped);
|
||||
thead.innerHTML = isGrouped ? groupedTheadHTML : flatTheadHTML;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('btn-filter').addEventListener('click', applyFilters);
|
||||
document.getElementById('btn-reset').addEventListener('click', function(){
|
||||
document.getElementById('f-da').value = sevenAgo;
|
||||
document.getElementById('f-a').value = today;
|
||||
document.getElementById('f-sid').value = '';
|
||||
document.querySelectorAll('.tipo-chk').forEach(function(c){ c.checked = true; });
|
||||
applyFilters();
|
||||
});
|
||||
document.querySelectorAll('.tipo-chk').forEach(function(c){
|
||||
c.addEventListener('change', applyFilters);
|
||||
});
|
||||
|
||||
applyFilters();
|
||||
})();
|
||||
|
||||
/* ── Docs modal ────────────────────────────────────────────────────────── */
|
||||
(function(){
|
||||
var modal = document.getElementById('docs-modal');
|
||||
var modalBody = document.getElementById('docs-modal-body');
|
||||
var modalSid = document.getElementById('docs-modal-sid');
|
||||
var modalCorso = document.getElementById('docs-modal-corso');
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
function openDocsModal(sid, corso) {
|
||||
modalSid.textContent = sid;
|
||||
modalCorso.textContent = corso || '';
|
||||
modalBody.innerHTML = '<p class="docs-loading">Caricamento\u2026</p>';
|
||||
modal.style.display = 'flex';
|
||||
|
||||
fetch('/api/docs?sessione_id=' + encodeURIComponent(sid))
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(docs) {
|
||||
if (!docs || docs.length === 0) {
|
||||
modalBody.innerHTML = '<p class="docs-empty">Nessun documento caricato.</p>';
|
||||
return;
|
||||
}
|
||||
var rows = docs.map(function(d) {
|
||||
var st = d.doc_status || '';
|
||||
var badge = '<span class="doc-status-badge doc-status-' + esc(st) + '">' + esc(st) + '</span>';
|
||||
var fn = d.filename || '';
|
||||
var fnHtml = esc(fn);
|
||||
if (fn.charAt(0) === '[' && fn.charAt(fn.length - 1) === ']')
|
||||
fnHtml = '<span style="color:#f97316;">' + fnHtml + '</span>';
|
||||
var name = d.sp_web_url
|
||||
? '<a href="' + d.sp_web_url + '" target="_blank" rel="noopener">' + fnHtml + '</a>'
|
||||
: fnHtml;
|
||||
var ts = d.uploaded_at ? d.uploaded_at.slice(0,16).replace('T',' ') : '-';
|
||||
return '<tr>'
|
||||
+ '<td class="doc-subfolder">' + esc(d.subfolder) + '</td>'
|
||||
+ '<td class="doc-filename">' + name + '</td>'
|
||||
+ '<td>' + badge + '</td>'
|
||||
+ '<td class="doc-date">' + ts + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
modalBody.innerHTML =
|
||||
'<table class="docs-table">'
|
||||
+ '<thead><tr><th>Cartella</th><th>File</th><th>Doc</th><th>Caricato SP</th></tr></thead>'
|
||||
+ '<tbody>' + rows + '</tbody>'
|
||||
+ '</table>';
|
||||
})
|
||||
.catch(function(err) {
|
||||
modalBody.innerHTML = '<p class="docs-error">Errore: ' + esc(String(err)) + '</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function closeDocsModal() { modal.style.display = 'none'; }
|
||||
|
||||
document.getElementById('docs-modal-close').addEventListener('click', closeDocsModal);
|
||||
modal.addEventListener('click', function(ev) { if (ev.target === modal) closeDocsModal(); });
|
||||
document.addEventListener('keydown', function(ev) { if (ev.key === 'Escape') closeDocsModal(); });
|
||||
|
||||
document.querySelector('.tw').addEventListener('click', function(ev) {
|
||||
var btn = ev.target.closest('.doc-btn');
|
||||
if (btn) openDocsModal(btn.getAttribute('data-sid'), btn.getAttribute('data-corso'));
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,14 @@
|
||||
(function(){
|
||||
var chkOk = document.getElementById('chk-completed');
|
||||
var chkErr = document.getElementById('chk-incomplete');
|
||||
function applyFilter() {
|
||||
var showOk = chkOk.checked;
|
||||
var showErr = chkErr.checked;
|
||||
document.querySelectorAll('tbody tr[data-completed]').forEach(function(tr) {
|
||||
var done = tr.getAttribute('data-completed') === '1';
|
||||
tr.style.display = (done ? showOk : showErr) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
chkOk.addEventListener('change', applyFilter);
|
||||
chkErr.addEventListener('change', applyFilter);
|
||||
})();
|
||||
@@ -0,0 +1,3 @@
|
||||
/* dashboard.css — stili specifici per la pagina Dashboard */
|
||||
.sec-link{color:inherit;text-decoration:none}
|
||||
.sec-link:hover{text-decoration:underline;text-underline-offset:3px}
|
||||
@@ -0,0 +1,125 @@
|
||||
/* ── documenti.css — session blocks, table, badges, print ── */
|
||||
|
||||
/* ── Session blocks ────────────────────────────── */
|
||||
.sess-block {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: .6rem;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sess-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
padding: .65rem 1rem;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.sess-summary:hover { background: #f1f5f9; }
|
||||
.sess-block[open] > .sess-summary { border-bottom: 1px solid #e2e8f0; }
|
||||
|
||||
.sess-id {
|
||||
font-weight: 700;
|
||||
font-size: .8rem;
|
||||
color: #2563eb;
|
||||
min-width: 56px;
|
||||
}
|
||||
.sess-corso {
|
||||
font-weight: 600;
|
||||
font-size: .875rem;
|
||||
color: #1e293b;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sess-azienda {
|
||||
font-size: .8rem;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
}
|
||||
.sess-counts { display: flex; gap: .35rem; flex-shrink: 0; }
|
||||
|
||||
/* ── Count badges ──────────────────────────────── */
|
||||
.cnt { font-size: .75rem; font-weight: 600; padding: 2px 8px; border-radius: 99px; }
|
||||
.cnt-caricato { background: #dcfce7; color: #15803d; }
|
||||
.cnt-trovato { background: #dbeafe; color: #1d4ed8; }
|
||||
.cnt-warning { background: #fef9c3; color: #a16207; }
|
||||
.cnt-errore { background: #fee2e2; color: #b91c1c; }
|
||||
|
||||
/* ── Status badges ─────────────────────────────── */
|
||||
.badge.doc-trovato { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge.doc-caricato { background: #dcfce7; color: #15803d; }
|
||||
.badge.doc-warning { background: #fef9c3; color: #a16207; }
|
||||
.badge.doc-errore { background: #fee2e2; color: #b91c1c; }
|
||||
|
||||
/* ── Subtable ──────────────────────────────────── */
|
||||
.sess-docs { padding: .5rem 1rem 1rem; overflow-x: auto; }
|
||||
|
||||
.doc-subtable { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: .85rem; }
|
||||
.doc-subtable thead th {
|
||||
text-align: left; padding: 6px 10px;
|
||||
border-bottom: 2px solid #e2e8f0; color: #475569; font-weight: 600; white-space: nowrap;
|
||||
}
|
||||
.doc-subtable tbody td { padding: 5px 10px; border-bottom: 1px solid #f1f5f9; color: #1e293b; }
|
||||
.doc-subtable tbody tr:last-child td { border-bottom: none; }
|
||||
.doc-subtable tbody tr:hover td { background: #f8fafc; }
|
||||
|
||||
.doc-row.doc-warning td { background: #fefce8; }
|
||||
.doc-row.doc-errore td { background: #fff5f5; }
|
||||
|
||||
.col-subfolder { color: #64748b; font-size: .78rem; white-space: nowrap; }
|
||||
.col-doctype { color: #475569; font-size: .8rem; white-space: nowrap; }
|
||||
.col-status { white-space: nowrap; }
|
||||
.col-filename { color: #1e293b; width: 30%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.col-note { color: #94a3b8; font-size: .8rem; max-height: 3.2em; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.col-sp { text-align: center; white-space: nowrap; }
|
||||
.sp-link { font-size: 1.1rem; text-decoration: none; }
|
||||
.sp-link:hover { opacity: .75; }
|
||||
.sp-missing { font-size: 1rem; color: #cbd5e1; }
|
||||
.col-uploaded { color: #94a3b8; font-size: .78rem; white-space: nowrap; }
|
||||
|
||||
.empty-msg { color: #64748b; padding: 2rem 0; text-align: center; }
|
||||
|
||||
/* pagination-hidden (by JS) */
|
||||
.pag-hidden { display: none; }
|
||||
|
||||
/* ── Print / PDF export ────────────────────────────────── */
|
||||
@media print {
|
||||
.print-hidden { display: none !important; }
|
||||
@page { size: A4 landscape; margin: 12mm 10mm; }
|
||||
header, nav, footer, .doc-toolbar, .floating-export { display: none !important; }
|
||||
main { padding: 0; margin: 0; }
|
||||
details.sess-block { display: block !important; }
|
||||
details.sess-block > .sess-docs { display: block !important; }
|
||||
.sess-block {
|
||||
border: 1px solid #cbd5e1; border-radius: 4px; margin-bottom: 0;
|
||||
page-break-before: always; page-break-inside: avoid; break-before: page; break-inside: avoid;
|
||||
}
|
||||
.sess-block:first-child { page-break-before: auto; break-before: auto; }
|
||||
.sess-summary { background: #f1f5f9 !important; border-bottom: 1px solid #cbd5e1; }
|
||||
.sess-docs { padding: .3rem .6rem .6rem; }
|
||||
.doc-subtable { width: 100%; font-size: .72rem; table-layout: fixed; }
|
||||
.doc-subtable thead th { background: #f8fafc; padding: 4px 6px; }
|
||||
.doc-subtable tbody td { padding: 3px 6px; }
|
||||
.col-filename { width: 22%; word-break: break-word; }
|
||||
.col-doctype { width: 11%; }
|
||||
.col-subfolder { width: 11%; }
|
||||
.col-status { width: 7%; }
|
||||
.col-note { width: 38%; word-break: break-word; -webkit-line-clamp: unset; max-height: none; }
|
||||
.col-uploaded { width: 9%; }
|
||||
.doc-row.doc-warning td { background: #fefce8 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.doc-row.doc-errore td { background: #fff5f5 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.badge { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.cnt { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.col-sp { display: none; }
|
||||
.doc-subtable thead th:nth-child(5) { display: none; }
|
||||
.pag-hidden { display: block !important; }
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/* ── documenti_filters.css — toolbar, filters, export, settings, pagination ── */
|
||||
|
||||
.doc-header {
|
||||
margin-bottom: 1rem;
|
||||
padding-top: 16px;
|
||||
/* sticky */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.06);
|
||||
padding-bottom: .6rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#doc-content { padding-top: 1rem; }
|
||||
|
||||
.doc-toolbar { display: flex; flex-direction: column; gap: .4rem; }
|
||||
|
||||
.doc-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: .6rem;
|
||||
}
|
||||
|
||||
.doc-search {
|
||||
padding: .4rem .75rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
font-size: .875rem;
|
||||
width: 260px;
|
||||
outline: none;
|
||||
}
|
||||
.doc-search:focus { border-color: #2563eb; box-shadow: 0 0 0 2px #dbeafe; }
|
||||
|
||||
.doc-filter-group { display: flex; align-items: center; gap: .5rem; }
|
||||
|
||||
.doc-count { margin-left: 0; font-size: .8rem; color: #64748b; }
|
||||
|
||||
.filter-label { font-size: .8rem; font-weight: 600; color: #475569; white-space: nowrap; min-width: 55px; }
|
||||
|
||||
.btn-refresh { background: #f0fdf4; border: 1px solid #86efac; color: #15803d; font-weight: 600; }
|
||||
.btn-refresh:hover { background: #dcfce7; }
|
||||
.btn-refresh:disabled { opacity: .6; cursor: default; }
|
||||
|
||||
.doc-refresh-ts { font-size: .75rem; color: #94a3b8; white-space: nowrap; }
|
||||
|
||||
.doc-action-group {
|
||||
display: flex; align-items: center; gap: .4rem;
|
||||
margin-left: auto;
|
||||
background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 7px; padding: 3px 6px;
|
||||
}
|
||||
|
||||
/* ── Export buttons ──────────────────────────────────────── */
|
||||
.btn-excel { display: flex; align-items: center; gap: .35rem; background: #16a34a; color: #fff; border-color: #16a34a; }
|
||||
.btn-excel:hover { background: #15803d; }
|
||||
.btn-pdf { display: flex; align-items: center; gap: .35rem; background: #2563eb; color: #fff; border-color: #2563eb; }
|
||||
.btn-pdf:hover { background: #1d4ed8; }
|
||||
.pdf-icon { width: 1em; height: 1em; flex-shrink: 0; }
|
||||
|
||||
/* ── Floating export ─────────────────────────────────────── */
|
||||
.floating-export {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: .5rem;
|
||||
z-index: 150;
|
||||
filter: drop-shadow(0 4px 12px rgba(0,0,0,.18));
|
||||
}
|
||||
|
||||
.floating-export-btns { display: flex; gap: .4rem; }
|
||||
|
||||
/* ── Page settings modal ─────────────────────────────────── */
|
||||
.page-settings-overlay {
|
||||
display: none; position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.35); z-index: 500;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.page-settings-overlay.open { display: flex; }
|
||||
|
||||
.page-settings-dialog {
|
||||
background: #fff; border-radius: 10px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.2); width: 300px; overflow: hidden;
|
||||
}
|
||||
.page-settings-hdr {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: .75rem 1rem; background: #f8fafc; border-bottom: 1px solid #e2e8f0;
|
||||
font-weight: 600; font-size: .9rem; color: #1e293b;
|
||||
}
|
||||
.page-settings-close {
|
||||
background: none; border: none; font-size: 1.25rem; line-height: 1;
|
||||
cursor: pointer; color: #94a3b8; padding: 0 2px;
|
||||
}
|
||||
.page-settings-close:hover { color: #1e293b; }
|
||||
.page-settings-body { padding: .9rem 1rem; display: flex; flex-direction: column; gap: .6rem; }
|
||||
.page-settings-row { display: flex; align-items: center; gap: .5rem; font-size: .875rem; color: #1e293b; }
|
||||
.page-settings-row label { display: flex; align-items: center; gap: .45rem; cursor: pointer; }
|
||||
.page-settings-row span { white-space: nowrap; flex: 1; color: #475569; }
|
||||
.page-settings-input {
|
||||
width: 72px; padding: .3rem .4rem;
|
||||
border: 1px solid #cbd5e1; border-radius: 5px; font-size: .875rem; outline: none;
|
||||
}
|
||||
.page-settings-input:focus { border-color: #2563eb; box-shadow: 0 0 0 2px #dbeafe; }
|
||||
|
||||
/* ── Arrow direction override (filters above the action row) ── */
|
||||
.filters-arrow { transform: rotate(270deg); } /* expanded = up */
|
||||
.filters-arrow.collapsed { transform: rotate(90deg); } /* collapsed = down */
|
||||
|
||||
/* ── Pagination ──────────────────────────────────────────── */
|
||||
#doc-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: .25rem;
|
||||
flex-wrap: wrap;
|
||||
background: rgba(255,255,255,.96);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: .4rem .6rem;
|
||||
max-width: calc(100vw - 3.5rem);
|
||||
}
|
||||
|
||||
.doc-pag-btn {
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0 .5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
font-size: .82rem;
|
||||
cursor: pointer;
|
||||
transition: background .1s, border-color .1s;
|
||||
line-height: 1;
|
||||
}
|
||||
.doc-pag-btn:hover:not([disabled]) { background: #f1f5f9; border-color: #94a3b8; }
|
||||
.doc-pag-btn.active { background: #2563eb; color: #fff; border-color: #1d4ed8; font-weight: 600; }
|
||||
.doc-pag-btn[disabled] { opacity: .35; cursor: default; }
|
||||
|
||||
.doc-pag-ellipsis { color: #94a3b8; font-size: .85rem; padding: 0 .2rem; }
|
||||
@@ -0,0 +1,50 @@
|
||||
/* ── filters.css — shared filter toggle (all pages) ── */
|
||||
|
||||
/* ── Sticky page header (filter bar) — shared across all pages ── */
|
||||
.page-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: var(--surf, #fff);
|
||||
border-bottom: 1px solid var(--brd, #e2e8f0);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.06);
|
||||
padding-bottom: .6rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.page-header + section { padding-top: 1rem; }
|
||||
|
||||
/* Wrapper that keeps the header row and collapsible in the same container */
|
||||
.page-filter-wrap { display: flex; flex-direction: column; }
|
||||
|
||||
.page-filter-header { display: flex; align-items: center; margin-bottom: .5rem; }
|
||||
|
||||
.btn-toggle-filters {
|
||||
display: flex; flex-direction: column; align-items: center; gap: .15rem;
|
||||
background: #2563eb; border: 1px solid #1d4ed8; border-radius: 6px;
|
||||
padding: .4rem .35rem; cursor: pointer; color: #fff;
|
||||
flex-shrink: 0; margin-left: auto;
|
||||
transition: background .12s, border-color .12s;
|
||||
}
|
||||
.btn-toggle-filters:hover { background: #1d4ed8; border-color: #1e40af; }
|
||||
|
||||
.filters-btn-text {
|
||||
writing-mode: vertical-rl; transform: rotate(180deg);
|
||||
font-size: .72rem; font-weight: 600; letter-spacing: .04em; user-select: none;
|
||||
}
|
||||
|
||||
.filters-arrow {
|
||||
width: .9em; height: .9em; flex-shrink: 0;
|
||||
transition: transform .22s ease; transform: rotate(180deg);
|
||||
}
|
||||
.filters-arrow.collapsed { transform: rotate(0deg); }
|
||||
|
||||
.filters-collapsible {
|
||||
display: grid; grid-template-rows: 1fr;
|
||||
clip-path: inset(0 0 0 0);
|
||||
transition: grid-template-rows .32s ease, clip-path .28s ease;
|
||||
}
|
||||
.filters-collapsible.hidden {
|
||||
grid-template-rows: 0fr;
|
||||
clip-path: inset(0 0 0 100%);
|
||||
}
|
||||
.filters-collapsible-inner { overflow: hidden; }
|
||||
@@ -0,0 +1,69 @@
|
||||
/* iscrizioni.css — Corsi Intraziendali filter bar */
|
||||
|
||||
.isc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.isc-header h2 { margin: 0; }
|
||||
|
||||
.isc-filters {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: .4rem 0;
|
||||
}
|
||||
|
||||
.isc-filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.isc-filter-checks {
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.isc-filter-checks label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.isc-filter-checks input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
accent-color: #2563eb;
|
||||
}
|
||||
|
||||
.isc-search {
|
||||
border: 1px solid var(--brd, #e2e8f0);
|
||||
border-radius: 6px;
|
||||
padding: .3rem .65rem;
|
||||
font-size: .9rem;
|
||||
outline: none;
|
||||
min-width: 200px;
|
||||
background: var(--bg, #fff);
|
||||
color: inherit;
|
||||
}
|
||||
.isc-search:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 2px rgba(37,99,235,.15);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* logs.css — stili specifici per la pagina Log DB */
|
||||
.log-filters{display:flex;align-items:center;gap:14px;margin-bottom:12px;flex-wrap:wrap}
|
||||
.log-filters input[type=search]{padding:6px 10px;border:1px solid var(--brd);border-radius:6px;font-size:.85rem;width:230px;background:var(--surf);color:var(--txt)}
|
||||
.log-filters label{display:flex;align-items:center;gap:5px;font-size:.85rem;cursor:pointer;color:var(--mut);user-select:none}
|
||||
.log-filters input[type=checkbox]{cursor:pointer;accent-color:var(--pri)}
|
||||
|
||||
/* pagination — floating center bottom */
|
||||
.log-pagination {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 150;
|
||||
background: rgba(255,255,255,.96);
|
||||
border: 1px solid var(--brd);
|
||||
border-radius: 8px;
|
||||
padding: .4rem .75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.18);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pg-btn{padding:5px 13px;border:1px solid var(--brd);border-radius:6px;font-size:.85rem;text-decoration:none;color:var(--txt);background:var(--surf);cursor:pointer}
|
||||
.pg-btn:hover{background:var(--pri);color:#fff;border-color:var(--pri)}
|
||||
.pg-btn.disabled{opacity:.35;pointer-events:none;cursor:default}
|
||||
.pg-select{padding:5px 8px;border:1px solid var(--brd);border-radius:6px;font-size:.85rem;background:var(--surf);color:var(--txt);cursor:pointer}
|
||||
.pg-info{font-size:.8rem;color:var(--mut);margin-left:4px}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* pec.css — stili specifici per la pagina PEC */
|
||||
|
||||
.pec-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.pec-header h2 { margin: 0; }
|
||||
|
||||
.pec-filter {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.pec-filter label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.pec-filter input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
accent-color: #2563eb;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
|
||||
.report-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: var(--surf, #fff);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.06);
|
||||
padding-bottom: .6rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.report-header + section { padding-top: 1rem; }
|
||||
|
||||
.report-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: .6rem 1rem;
|
||||
}
|
||||
|
||||
.toolbar-row1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.floating-report-ctrl {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.75rem;
|
||||
z-index: 150;
|
||||
filter: drop-shadow(0 4px 12px rgba(0,0,0,.18));
|
||||
}
|
||||
|
||||
.toolbar-right-row1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Left side: groups of filters */
|
||||
.report-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* A visual group (date group, checkbox group) */
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: .3rem .6rem;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
font-size: .83rem;
|
||||
color: #475569;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-label input[type=date] {
|
||||
padding: .2rem .4rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
font-size: .83rem;
|
||||
background: #fff;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.chk-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
font-size: .83rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Action buttons inside the left side */
|
||||
.btn-action {
|
||||
padding: .28rem .7rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
font-size: .83rem;
|
||||
cursor: pointer;
|
||||
transition: background .12s, border-color .12s;
|
||||
}
|
||||
.btn-action:hover { background: #f1f5f9; border-color: #94a3b8; }
|
||||
.btn-action.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
.btn-action.btn-primary:hover { background: #1d4ed8; }
|
||||
|
||||
/* Row count pill */
|
||||
.report-count {
|
||||
font-size: .78rem;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 999px;
|
||||
padding: .15rem .6rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Toggle button ───────────────────────────────────────────────────────── */
|
||||
.btn-toggle {
|
||||
padding: .3rem .85rem;
|
||||
border: 1px solid #1d4ed8;
|
||||
border-radius: 6px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
font-size: .83rem;
|
||||
cursor: pointer;
|
||||
transition: background .12s, border-color .12s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-toggle:hover { background: #1d4ed8; }
|
||||
.btn-toggle.active { background: #1e40af; border-color: #1e3a8a; }
|
||||
|
||||
/* ── Table cells ─────────────────────────────────────────────────────────── */
|
||||
td.val-prec { color: #dc2626; }
|
||||
td.val-nuovo { color: #16a34a; }
|
||||
td.col-prima-lez { color: #64748b; font-size: .8rem; white-space: nowrap; }
|
||||
.val-prec { color: #dc2626; }
|
||||
.val-nuovo { color: #16a34a; }
|
||||
|
||||
/* ── Grouped view: mod-cell ──────────────────────────────────────────────── */
|
||||
.mod-cell { min-width: 220px; }
|
||||
.mod-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .25rem;
|
||||
align-items: baseline;
|
||||
font-size: .81rem;
|
||||
padding: .15rem 0;
|
||||
border-bottom: 1px dotted #e2e8f0;
|
||||
}
|
||||
.mod-row:last-child { border-bottom: none; }
|
||||
.mod-campo { font-weight: 600; color: #475569; }
|
||||
.mod-none { color: #94a3b8; }
|
||||
|
||||
/* ── Docs modal ──────────────────────────────────────────────────────────── */
|
||||
.doc-btn { background: none; border: none; cursor: pointer; font-size: 1rem; opacity: .7; padding: 2px 4px; border-radius: 4px; }
|
||||
.doc-btn:hover { opacity: 1; background: #f1f5f9; }
|
||||
.docs-table { width: 100%; border-collapse: collapse; font-size: .875rem; }
|
||||
.docs-table th { text-align: left; padding: 6px 10px; border-bottom: 2px solid #e2e8f0; color: #475569; font-weight: 600; }
|
||||
.docs-table td { padding: 6px 10px; border-bottom: 1px solid #f1f5f9; color: #1e293b; }
|
||||
.docs-table tr:last-child td { border-bottom: none; }
|
||||
.doc-status-badge { display: inline-block; font-size: .75rem; font-weight: 600; padding: 2px 7px; border-radius: 99px; white-space: nowrap; }
|
||||
.doc-status-trovato { background: #dbeafe; color: #1d4ed8; }
|
||||
.doc-status-caricato { background: #dcfce7; color: #15803d; }
|
||||
.doc-status-warning { background: #fef9c3; color: #a16207; }
|
||||
.doc-status-errore { background: #fee2e2; color: #b91c1c; }
|
||||
.doc-subfolder { color: #64748b; font-size: .8rem; white-space: nowrap; }
|
||||
.doc-filename a { color: #2563eb; text-decoration: none; }
|
||||
.doc-filename a:hover { text-decoration: underline; }
|
||||
.doc-date { color: #94a3b8; font-size: .8rem; white-space: nowrap; }
|
||||
.docs-loading, .docs-empty, .docs-error { color: #64748b; font-size: .9rem; padding: 12px 0; }
|
||||
.docs-error { color: #dc2626; }
|
||||
@@ -0,0 +1,30 @@
|
||||
/* runs.css — stili specifici per la pagina Processi */
|
||||
|
||||
.runs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.runs-header h2 { margin: 0; }
|
||||
|
||||
.runs-filter {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.runs-filter label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.runs-filter input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
accent-color: #2563eb;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/* server-logs.css — stili specifici per la pagina Server Logs */
|
||||
@@ -0,0 +1,62 @@
|
||||
:root{--bg:#f1f5f9;--surf:#fff;--brd:#e2e8f0;--txt:#1e293b;--mut:#64748b;
|
||||
--pri:#2563eb;--ok:#16a34a;--warn:#d97706;--err:#dc2626;--lbl:#0284c7;--nav:#1d4ed8}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%}
|
||||
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--txt);font-size:14px;line-height:1.5;display:flex;flex-direction:column;overflow:hidden}
|
||||
header{background:var(--pri);color:#fff;padding:18px 32px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}
|
||||
header h1{font-size:1.3rem;font-weight:600}
|
||||
header .ts{font-size:.82rem;opacity:.8}
|
||||
section{margin-bottom:38px}
|
||||
h2{font-size:1rem;font-weight:600;margin-bottom:12px;color:var(--pri);
|
||||
border-left:3px solid var(--pri);padding-left:10px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-bottom:28px}
|
||||
.card{background:var(--surf);border:1px solid var(--brd);border-radius:8px;padding:14px 18px;text-align:center}
|
||||
.cv{font-size:1.9rem;font-weight:700}
|
||||
.cl{font-size:.75rem;color:var(--mut);margin-top:4px}
|
||||
.tw{overflow-x:auto;background:var(--surf);border:1px solid var(--brd);border-radius:8px}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
thead tr{background:#f8fafc}
|
||||
th{padding:9px 13px;text-align:left;font-size:.73rem;text-transform:uppercase;
|
||||
letter-spacing:.05em;color:var(--mut);border-bottom:1px solid var(--brd);white-space:nowrap}
|
||||
td{padding:8px 13px;border-bottom:1px solid var(--brd);vertical-align:top}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#f8fafc}
|
||||
.nc{max-width:260px;word-break:break-word;color:var(--mut)}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:.72rem;font-weight:600;white-space:nowrap}
|
||||
.ok {background:#dcfce7;color:var(--ok)}
|
||||
.warn{background:#fef9c3;color:var(--warn)}
|
||||
.err {background:#fee2e2;color:var(--err)}
|
||||
.info{background:#dbeafe;color:#1d4ed8}
|
||||
.proc{background:#ede9fe;color:#6d28d9}
|
||||
.log {background:#e0f2fe;color:var(--lbl)}
|
||||
.empty{color:var(--mut);font-style:italic;padding:14px}
|
||||
footer{text-align:center;color:var(--mut);font-size:.78rem;padding:16px 0 36px}
|
||||
main{max-width:1400px;width:100%;margin:0 auto;padding:24px 32px;flex:1;overflow-y:auto}
|
||||
/* header left nav (Dashboard / Processi RPA / Log DB) */
|
||||
.hdr-left{display:flex;align-items:center;gap:4px;margin-right:24px}
|
||||
.hdr-nav{color:rgba(255,255,255,.75);text-decoration:none;padding:6px 14px;font-size:.82rem;
|
||||
font-weight:500;border-radius:4px;transition:background .15s,opacity .15s}
|
||||
.hdr-nav:hover{color:#fff;background:rgba(255,255,255,.15)}
|
||||
.hdr-nav.active{color:#fff;background:rgba(255,255,255,.22);font-weight:700}
|
||||
/* content nav (Documenti / Iscrizioni / Report / PEC) */
|
||||
nav{background:var(--nav);display:flex;gap:2px;padding:0 32px;flex-shrink:0}
|
||||
nav a{color:#fff;text-decoration:none;padding:10px 18px;font-size:.85rem;font-weight:500;
|
||||
border-radius:4px 4px 0 0;opacity:.75;transition:opacity .15s}
|
||||
nav a:hover{opacity:1}
|
||||
nav a.active{opacity:1;background:var(--bg);color:var(--pri)}
|
||||
/* header right */
|
||||
.hdr-right{display:flex;align-items:center;gap:16px}
|
||||
/* refresh button */
|
||||
.refresh-btn{background:none;border:none;color:#fff;font-size:1.25rem;cursor:pointer;padding:4px 8px;opacity:.8;transition:opacity .15s,transform .3s;line-height:1;border-radius:4px}
|
||||
.refresh-btn:hover{opacity:1;background:rgba(255,255,255,.15);transform:rotate(180deg)}
|
||||
/* wrench menu */
|
||||
.wrench-menu{position:relative}
|
||||
.wrench-btn{background:none;border:none;color:#fff;font-size:1.15rem;cursor:pointer;padding:4px 8px;opacity:.8;transition:opacity .15s;line-height:1;border-radius:4px}
|
||||
.wrench-btn:hover{opacity:1;background:rgba(255,255,255,.15)}
|
||||
.wrench-drop{display:none;position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid var(--brd);border-radius:6px;min-width:175px;box-shadow:0 4px 14px rgba(0,0,0,.15);z-index:100}
|
||||
.wrench-drop.open{display:block}
|
||||
.wrench-drop a{display:block;padding:10px 16px;color:var(--txt);text-decoration:none;font-size:.85rem;white-space:nowrap}
|
||||
.wrench-drop a:hover{background:var(--bg);border-radius:6px}
|
||||
/* sort indicators */
|
||||
th.sort-asc::after{content:' \25B2';font-size:.65rem;opacity:.7}
|
||||
th.sort-desc::after{content:' \25BC';font-size:.65rem;opacity:.7}
|
||||
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>${title} — RPA Report</title>
|
||||
<style>${css}</style>
|
||||
<style>${page_css}</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hdr-left">
|
||||
<a href="/" class="hdr-nav ${nav_dashboard}">Dashboard</a>
|
||||
<a href="/runs" class="hdr-nav ${nav_runs}">Processi RPA</a>
|
||||
<a href="/logs" class="hdr-nav ${nav_logs}">Log DB</a>
|
||||
</div>
|
||||
<h1>${h1_title}</h1>
|
||||
<div class="hdr-right">
|
||||
<span class="ts">${now} | ${db_name}</span>
|
||||
<button class="refresh-btn" title="Aggiorna dati" onclick="location.reload()">↻</button>
|
||||
<div class="wrench-menu">
|
||||
<button class="wrench-btn" title="Impostazioni">⚙</button>
|
||||
<div class="wrench-drop">
|
||||
<a href="/change-db">📂 Cambia DB…</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<nav>
|
||||
${nav_step_link}
|
||||
${nav_report_link}
|
||||
${nav_pec_link}
|
||||
</nav>
|
||||
<main>
|
||||
${content}
|
||||
</main>
|
||||
<footer>RPA Process Dashboard — ${now} • ATG</footer>
|
||||
<script>${page_js}</script>
|
||||
<script>
|
||||
(function(){
|
||||
var btn = document.querySelector('.wrench-btn');
|
||||
var drop = document.querySelector('.wrench-drop');
|
||||
if(btn && drop){
|
||||
btn.addEventListener('click', function(e){
|
||||
e.stopPropagation();
|
||||
drop.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('click', function(){
|
||||
drop.classList.remove('open');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
document.querySelectorAll('th').forEach(function(th, i) {
|
||||
th.style.cursor = 'pointer';
|
||||
th.title = 'Clicca per ordinare';
|
||||
var asc = true;
|
||||
th.addEventListener('click', function() {
|
||||
var tbody = th.closest('table').querySelector('tbody');
|
||||
var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
|
||||
rows.sort(function(a, b) {
|
||||
var av = (a.cells[i] ? a.cells[i].textContent : '').trim();
|
||||
var bv = (b.cells[i] ? b.cells[i].textContent : '').trim();
|
||||
return asc ? av.localeCompare(bv, 'it', {numeric: true}) : bv.localeCompare(av, 'it', {numeric: true});
|
||||
});
|
||||
rows.forEach(function(r) { tbody.appendChild(r); });
|
||||
th.closest('thead').querySelectorAll('th').forEach(function(h) { h.classList.remove('sort-asc','sort-desc'); });
|
||||
th.classList.add(asc ? 'sort-asc' : 'sort-desc');
|
||||
asc = !asc;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
<section>
|
||||
<h2><a href="/runs" class="sec-link">▶ Processi RPA</a></h2>
|
||||
<div class="grid">${cards_runs}</div>
|
||||
</section>
|
||||
|
||||
${section_middle}
|
||||
|
||||
<section>
|
||||
<h2><a href="/logs" class="sec-link">📄 Log DB</a></h2>
|
||||
<div class="grid">${cards_logs}</div>
|
||||
</section>
|
||||
@@ -0,0 +1,50 @@
|
||||
<section class="doc-header">
|
||||
<div class="doc-toolbar">
|
||||
<div id="filters-collapsible" class="filters-collapsible">
|
||||
<div class="filters-collapsible-inner">
|
||||
<div class="doc-toolbar-row">
|
||||
<input type="text" id="doc-search" placeholder="Cerca ID sessione, corso, azienda…" class="doc-search">
|
||||
<div class="doc-filter-group">
|
||||
<label class="chk-label"><input type="checkbox" class="status-chk" value="trovato" checked> <span class="badge doc-trovato">trovato</span></label>
|
||||
<label class="chk-label"><input type="checkbox" class="status-chk" value="caricato" checked> <span class="badge doc-caricato">caricato</span></label>
|
||||
<label class="chk-label"><input type="checkbox" class="status-chk" value="warning" checked> <span class="badge doc-warning">warning</span></label>
|
||||
<label class="chk-label"><input type="checkbox" class="status-chk" value="errore" checked> <span class="badge doc-errore">errore</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-toolbar-row">
|
||||
<span class="filter-label">Tipo:</span>
|
||||
<div class="doc-filter-group" id="filter-tipo"></div>
|
||||
</div>
|
||||
<div class="doc-toolbar-row">
|
||||
<span class="filter-label">Cartella:</span>
|
||||
<div class="doc-filter-group" id="filter-cartella"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-toolbar-row">
|
||||
<div class="doc-action-group">
|
||||
<button id="btn-refresh" class="btn-action btn-refresh">↻ Aggiorna dati</button>
|
||||
<span id="doc-refresh-ts" class="doc-refresh-ts"></span>
|
||||
<span class="doc-count" id="doc-visible-count"></span>
|
||||
<button id="btn-expand-all" class="btn-action">Espandi tutti</button>
|
||||
<button id="btn-collapse-all" class="btn-action">Comprimi tutti</button>
|
||||
</div>
|
||||
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
|
||||
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
<span class="filters-btn-text">Filtri</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="doc-content">
|
||||
${sessions_html}
|
||||
</section>
|
||||
|
||||
<div class="floating-export">
|
||||
<div id="doc-pagination"></div>
|
||||
<div class="floating-export-btns">
|
||||
<button id="btn-excel" class="btn-action btn-excel"><svg class="pdf-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7l4 5 4-5M8 17l4-5 4 5"/></svg> Excel</button>
|
||||
<button id="btn-pdf" class="btn-action btn-pdf"><svg class="pdf-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="13" y2="17"/></svg> PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<section class="page-header">
|
||||
<div class="page-filter-wrap">
|
||||
<div class="isc-header">
|
||||
<h2>📋 Corsi Intraziendali</h2>
|
||||
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
|
||||
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
<span class="filters-btn-text">Filtri</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="filters-collapsible" class="filters-collapsible">
|
||||
<div class="filters-collapsible-inner">
|
||||
<div class="isc-filters">
|
||||
<div class="isc-filter-group">
|
||||
<span class="filter-label">Cerca:</span>
|
||||
<input type="text" id="isc-search" class="isc-search" placeholder="Azienda, corso…">
|
||||
</div>
|
||||
<div class="isc-filter-group">
|
||||
<span class="filter-label">Stato:</span>
|
||||
<div class="isc-filter-checks" id="filter-stato">${filter_stato_html}</div>
|
||||
</div>
|
||||
<div class="isc-filter-group" id="filter-sorgente-wrap">
|
||||
<span class="filter-label">Sorgente:</span>
|
||||
<div class="isc-filter-checks" id="filter-sorgente"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="tw">${tbl_iscrizioni}</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
<section class="page-header">
|
||||
<div class="page-filter-wrap">
|
||||
<div class="page-filter-header">
|
||||
<h2>Log DB — ${run_info}</h2>
|
||||
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
|
||||
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
<span class="filters-btn-text">Filtri</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="filters-collapsible" class="filters-collapsible">
|
||||
<div class="filters-collapsible-inner">
|
||||
<div class="log-filters">
|
||||
<input type="search" id="log-search" placeholder="Cerca nei log…" />
|
||||
<label><input type="checkbox" class="type-filter" value="log" checked> Log</label>
|
||||
<label><input type="checkbox" class="type-filter" value="warning" checked> Warning</label>
|
||||
<label><input type="checkbox" class="type-filter" value="error" checked> Error</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="tw">${tbl_logs}</div>
|
||||
</section>
|
||||
${pagination}
|
||||
@@ -0,0 +1,23 @@
|
||||
<section class="page-header">
|
||||
<div class="page-filter-wrap">
|
||||
<div class="pec-header">
|
||||
<h2>Comunicazioni PEC (ultime 200)</h2>
|
||||
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
|
||||
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
<span class="filters-btn-text">Filtri</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="filters-collapsible" class="filters-collapsible">
|
||||
<div class="filters-collapsible-inner">
|
||||
<div class="pec-filter">
|
||||
<label><input type="checkbox" data-stato="inviato" checked> Inviato</label>
|
||||
<label><input type="checkbox" data-stato="pending" checked> Pending</label>
|
||||
<label><input type="checkbox" data-stato="errore" checked> Errore</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="tw">${tbl_pec}</div>
|
||||
</section>
|
||||
@@ -0,0 +1,48 @@
|
||||
<section class="report-header">
|
||||
<div class="report-toolbar">
|
||||
<div class="toolbar-row1">
|
||||
<div class="toolbar-right-row1">
|
||||
<button id="btn-reset" class="btn-action">Reset</button>
|
||||
<span class="report-count" id="visible-count"></span>
|
||||
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
|
||||
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
<span class="filters-btn-text">Filtri</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="filters-collapsible" class="filters-collapsible">
|
||||
<div class="filters-collapsible-inner">
|
||||
<div class="report-filters">
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Dal <input type="date" id="f-da"></label>
|
||||
<label class="filter-label">Al <input type="date" id="f-a"></label>
|
||||
<label class="filter-label">ID Sessione <input type="text" id="f-sid" placeholder="es. 123" style="width:80px;padding:.3rem .5rem;border:1px solid #cbd5e1;border-radius:6px;font-size:.875rem;"></label>
|
||||
<button id="btn-filter" class="btn-action btn-primary">Filtra</button>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="chk-label"><input type="checkbox" class="tipo-chk" value="nuovo" checked> <span class="badge ok">Nuovo</span></label>
|
||||
<label class="chk-label"><input type="checkbox" class="tipo-chk" value="modificato" checked> <span class="badge warn">Modificato</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="tw">${tbl_report}</div>
|
||||
</section>
|
||||
|
||||
<div class="floating-report-ctrl">
|
||||
<button id="btn-toggle-view" class="btn-toggle">⧧ Raggruppa corso</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Docs Modal ───────────────────────────────────────────────────────── -->
|
||||
<div id="docs-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:8px;padding:24px;min-width:520px;max-width:80vw;max-height:80vh;display:flex;flex-direction:column;gap:12px;box-shadow:0 8px 32px rgba(0,0,0,.25);">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<h3 style="margin:0;font-size:1rem;color:#1e293b;">📄 Documenti — <span id="docs-modal-corso"></span><br><small style="font-weight:400;color:#64748b;">sessione <span id="docs-modal-sid" style="color:#2563eb;"></span></small></h3>
|
||||
<button id="docs-modal-close" style="background:none;border:none;font-size:1.4rem;cursor:pointer;color:#64748b;line-height:1;" title="Chiudi">×</button>
|
||||
</div>
|
||||
<div id="docs-modal-body" style="overflow-y:auto;flex:1;"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<section class="page-header">
|
||||
<div class="page-filter-wrap">
|
||||
<div class="runs-header">
|
||||
<h2>Processi RPA (ultimi 50)</h2>
|
||||
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
|
||||
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
|
||||
<span class="filters-btn-text">Filtri</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="filters-collapsible" class="filters-collapsible">
|
||||
<div class="filters-collapsible-inner">
|
||||
<div class="runs-filter" id="runs-filter">
|
||||
<label><input type="checkbox" id="chk-completed" checked> Completati</label>
|
||||
<label><input type="checkbox" id="chk-incomplete" checked> Incompleti</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="tw">${tbl_processes}</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user