Iscrizioni fix, new graph and sharepoint page
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json as _json
|
||||
from .config import _state, _access_log, DbType
|
||||
from .renderer import _base, _card, _page_css, _page_js, _tmpl
|
||||
from .queries.db import e as _e, query as _query
|
||||
@@ -9,6 +10,7 @@ from .queries import documenti as q_documenti
|
||||
from .queries import iscrizioni as q_iscrizioni
|
||||
from .queries import api_iscrizioni as q_api_iscrizioni
|
||||
from .queries import email as q_email
|
||||
from .queries import sharepoint as q_sharepoint
|
||||
|
||||
|
||||
def _load_data(db_path: str) -> dict:
|
||||
@@ -187,6 +189,14 @@ def _page_api_iscrizioni(db_path: str) -> str:
|
||||
return _base('Iscrizioni', content, 'nav_api_iscrizioni', db_path, _page_css('api_iscrizioni'), _page_js('api_iscrizioni'))
|
||||
|
||||
|
||||
def _page_sharepoint(db_path: str) -> str:
|
||||
rows = q_sharepoint.get_sharepoint(db_path)
|
||||
content = _tmpl('sharepoint.html').substitute(
|
||||
tbl_sharepoint=q_sharepoint.render_table(rows)
|
||||
)
|
||||
return _base('SharePoint', content, 'nav_sharepoint', db_path, _page_css('sharepoint'), _page_js('sharepoint'))
|
||||
|
||||
|
||||
def _page_email(db_path: str) -> str:
|
||||
rows = q_email.get_email(db_path)
|
||||
content = _tmpl('email.html').substitute(
|
||||
@@ -195,6 +205,19 @@ def _page_email(db_path: str) -> str:
|
||||
return _base('Email', content, 'nav_email', db_path, _page_css('email'), _page_js('email'))
|
||||
|
||||
|
||||
def _page_schema(db_path: str) -> str:
|
||||
rows = q_processes.get_schema_stats(db_path)
|
||||
labels = [r.get('process_name') or 'unknown' for r in rows]
|
||||
counts = [r.get('run_count', 0) for r in rows]
|
||||
minutes = [max(1, (r.get('total_seconds') or 0) // 60) for r in rows]
|
||||
content = _tmpl('schema.html').substitute(
|
||||
labels_json=_json.dumps(labels),
|
||||
counts_json=_json.dumps(counts),
|
||||
minutes_json=_json.dumps(minutes),
|
||||
)
|
||||
return _base('Schema', content, 'nav_schema', db_path, _page_css('schema'), _page_js('schema'))
|
||||
|
||||
|
||||
def _page_server_logs(db_path: str) -> str:
|
||||
rows = ''.join(
|
||||
f'<tr>'
|
||||
|
||||
+35
-26
@@ -29,6 +29,7 @@ def get_corsi(db_path: str) -> list:
|
||||
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,
|
||||
bc.odv_number,
|
||||
s.step_ftp, s.err_ftp,
|
||||
s.step_bc, s.err_bc,
|
||||
s.step_iscrizione, s.err_iscrizione,
|
||||
@@ -38,8 +39,9 @@ def get_corsi(db_path: str) -> list:
|
||||
s.step_email, s.err_email,
|
||||
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_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
|
||||
LEFT JOIN rpa_intra_bc_check bc ON bc.rpa_intra_ftp_json_id = f.id
|
||||
ORDER BY f.id DESC
|
||||
""")
|
||||
|
||||
@@ -82,15 +84,16 @@ def _row_status(r: dict) -> str:
|
||||
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'
|
||||
if any(r.get(s) == 'pending' for s in _ALL_STEPS):
|
||||
return 'pending'
|
||||
return 'skipped'
|
||||
|
||||
|
||||
|
||||
|
||||
def _fth(label: str, col: str) -> str:
|
||||
return f'<th>{label}<button class="col-filter-btn" data-col="{col}" title="Filtra {label}">⚙</button></th>'
|
||||
def _fth(label: str, col: str, step: bool = False, data: bool = False) -> str:
|
||||
cls = ' class="step-th"' if step else (' class="data-th"' if data else '')
|
||||
return f'<th{cls}>{label}<button class="col-filter-btn" data-col="{col}" title="Filtra {label}">⚙</button></th>'
|
||||
|
||||
|
||||
def _clip(text: str, n: int = 20) -> str:
|
||||
@@ -116,6 +119,7 @@ def render_table(rows: list) -> str:
|
||||
f' data-azienda="{e(r.get("azienda_nome") or "")}"'
|
||||
f' data-corso="{e(r.get("corso_descrizione") or "")}"'
|
||||
f' data-data="{e(r.get("corso_data") or "")}"'
|
||||
f' data-odv_number="{e(r.get("odv_number") or "")}"'
|
||||
f' data-ftp="{e(r.get("step_ftp") or "pending")}"'
|
||||
f' data-bc="{e(r.get("step_bc") or "pending")}"'
|
||||
f' data-iscrizione="{e(r.get("step_iscrizione") or "pending")}"'
|
||||
@@ -124,39 +128,44 @@ def render_table(rows: list) -> str:
|
||||
f' data-report="{e(r.get("step_report") or "pending")}"'
|
||||
f' data-email="{e(r.get("step_email") or "pending")}"'
|
||||
f'>'
|
||||
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">{_clip(r.get("corso_descrizione") or "")}</td>'
|
||||
f'<td>{e(r.get("corso_data"))}</td>'
|
||||
f'<td>{e(r.get("n_iscritti"))}</td>'
|
||||
f'<td class="td-id">{e(r.get("id"))}'
|
||||
+ (f'<a href="/logs?run={e(r.get("rpa_process_id"))}" class="btn-log" title="Vedi log">☰</a>'
|
||||
if r.get('rpa_process_id') else '')
|
||||
+ f'</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_ftp"), r.get("err_ftp"))}</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_bc"), r.get("err_bc"))}</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_iscrizione"), r.get("err_iscrizione"))}</td>'
|
||||
f'<td class="step-td">{e(r.get("odv_number") or "-")}</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_odv"), r.get("err_odv"))}</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_sharepoint"), r.get("err_sharepoint"))}</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_report"), r.get("err_report"))}</td>'
|
||||
f'<td class="step-td">{_step_cell(r.get("step_email"), r.get("err_email"))}</td>'
|
||||
f'<td class="data-td">{e(r.get("sorgente"))}</td>'
|
||||
f'<td class="data-td">{e(r.get("azienda_nome"))}</td>'
|
||||
f'<td class="data-td">{e(r.get("sessione_id"))}</td>'
|
||||
f'<td class="data-td nc">{_clip(r.get("corso_descrizione") or "")}</td>'
|
||||
f'<td class="data-td">{e(r.get("corso_data"))}</td>'
|
||||
f'<td class="data-td">{e(r.get("n_iscritti"))}</td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
f'<th>#</th>'
|
||||
f'{_fth("Sorgente", "sorgente")}'
|
||||
f'{_fth("Azienda", "azienda")}'
|
||||
f'{_fth("Sessione", "sessione")}'
|
||||
f'{_fth("Corso", "corso")}'
|
||||
f'{_fth("Data", "data")}'
|
||||
f'<th>N.</th>'
|
||||
f'{_fth("FTP", "ftp")}'
|
||||
f'{_fth("BC", "bc")}'
|
||||
f'{_fth("Iscrizione", "iscrizione")}'
|
||||
f'{_fth("ODV", "odv")}'
|
||||
f'{_fth("SharePoint", "sharepoint")}'
|
||||
f'{_fth("Report", "report")}'
|
||||
f'{_fth("Email", "email")}'
|
||||
f'{_fth("FTP", "ftp", step=True)}'
|
||||
f'{_fth("BC", "bc", step=True)}'
|
||||
f'{_fth("Iscrizione", "iscrizione", step=True)}'
|
||||
f'{_fth("ODV Number", "odv_number", step=True)}'
|
||||
f'{_fth("ODV Step", "odv", step=True)}'
|
||||
f'{_fth("SharePoint", "sharepoint", step=True)}'
|
||||
f'{_fth("Report", "report", step=True)}'
|
||||
f'{_fth("Email", "email", step=True)}'
|
||||
f'{_fth("Sorgente", "sorgente", data=True)}'
|
||||
f'{_fth("Azienda", "azienda", data=True)}'
|
||||
f'{_fth("Sessione", "sessione", data=True)}'
|
||||
f'{_fth("Corso", "corso", data=True)}'
|
||||
f'{_fth("Data", "data", data=True)}'
|
||||
f'<th class="data-th">N. iscritti</th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from .db import query, count, e, dt, dur, badge
|
||||
|
||||
|
||||
def get_processes(db_path: str) -> list:
|
||||
return query(db_path, "SELECT id, process_name, note, completed, start_run, finish_run FROM rpa_process ORDER BY start_run DESC LIMIT 50")
|
||||
return query(db_path, "SELECT id, process_name, note, completed, start_run, finish_run FROM rpa_process ORDER BY id DESC LIMIT 50")
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
@@ -17,6 +17,19 @@ def get_stats(db_path: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def get_schema_stats(db_path: str) -> list:
|
||||
return query(db_path, """
|
||||
SELECT process_name,
|
||||
COUNT(*) as run_count,
|
||||
SUM(CASE WHEN start_run IS NOT NULL AND finish_run IS NOT NULL
|
||||
THEN CAST((julianday(finish_run) - julianday(start_run)) * 86400 AS INTEGER)
|
||||
ELSE 0 END) as total_seconds
|
||||
FROM rpa_process
|
||||
GROUP BY process_name
|
||||
ORDER BY run_count DESC
|
||||
""")
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty">Nessun processo trovato.</p>'
|
||||
@@ -33,11 +46,12 @@ def render_table(rows: list) -> str:
|
||||
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'<td><a href="/logs?run={e(r.get("id"))}" class="btn-log">→ Log</a></td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
'<th>Processo</th><th>Inizio</th><th>Fine</th><th>Durata</th><th>Stato</th><th>Note</th>'
|
||||
'<th>Processo</th><th>Inizio</th><th>Fine</th><th>Durata</th><th>Stato</th><th>Note</th><th></th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""queries/sharepoint.py — rpa_intra_sharepoint (Intraz)"""
|
||||
|
||||
from .db import query, count, e, dt, badge
|
||||
|
||||
|
||||
def get_sharepoint(db_path: str) -> list:
|
||||
return query(db_path, """
|
||||
SELECT
|
||||
sp.id, sp.rpa_intra_ftp_json_id, sp.rpa_process_id,
|
||||
sp.file_type, sp.remote_path, sp.stato, sp.note,
|
||||
sp.created_at, sp.updated_at,
|
||||
json_extract(f.corso, '$.IDsessione') AS sessione_id,
|
||||
json_extract(f.corso, '$.descrizione') AS corso_descrizione,
|
||||
json_extract(f.azienda, '$.ragione_sociale') AS azienda_nome
|
||||
FROM rpa_intra_sharepoint sp
|
||||
LEFT JOIN rpa_intra_ftp_json f ON f.id = sp.rpa_intra_ftp_json_id
|
||||
ORDER BY sp.id DESC
|
||||
LIMIT 500
|
||||
""")
|
||||
|
||||
|
||||
def get_stats(db_path: str) -> dict:
|
||||
return {
|
||||
'sp_total': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint"),
|
||||
'sp_uploaded': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint WHERE stato='uploaded'"),
|
||||
'sp_errore': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint WHERE stato='errore'"),
|
||||
'sp_pending': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint WHERE stato='pending'"),
|
||||
}
|
||||
|
||||
|
||||
def render_table(rows: list) -> str:
|
||||
if not rows:
|
||||
return '<p class="empty">Nessun file SharePoint trovato.</p>'
|
||||
|
||||
def _badge(stato):
|
||||
if stato == 'uploaded': return badge('ok', 'Uploaded')
|
||||
if stato == 'errore': return badge('err', 'Errore')
|
||||
return badge('warn', 'Pending')
|
||||
|
||||
trs = ''.join(
|
||||
f'<tr data-stato="{e(r.get("stato") or "pending")}">'
|
||||
f'<td>{e(r.get("id"))}</td>'
|
||||
f'<td>{e(r.get("rpa_intra_ftp_json_id"))}</td>'
|
||||
f'<td>{e(r.get("sessione_id"))}</td>'
|
||||
f'<td>{e(r.get("azienda_nome"))}</td>'
|
||||
f'<td>{e(r.get("file_type"))}</td>'
|
||||
f'<td class="nc">{e(r.get("remote_path"))}</td>'
|
||||
f'<td>{_badge(r.get("stato"))}</td>'
|
||||
f'<td class="nc">{e(r.get("note"))}</td>'
|
||||
f'<td>{dt(r.get("created_at"))}</td>'
|
||||
f'<td>{dt(r.get("updated_at"))}</td>'
|
||||
f'</tr>'
|
||||
for r in rows
|
||||
)
|
||||
return (
|
||||
'<table><thead><tr>'
|
||||
'<th>#</th><th>JSON ID</th><th>Sessione</th><th>Azienda</th>'
|
||||
'<th>Tipo</th><th>Remote Path</th><th>Stato</th><th>Note</th>'
|
||||
'<th>Creato</th><th>Aggiornato</th>'
|
||||
f'</tr></thead><tbody>{trs}</tbody></table>'
|
||||
)
|
||||
+6
-2
@@ -46,7 +46,7 @@ def _card(label, value, color='#2563eb') -> str:
|
||||
|
||||
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': ''}
|
||||
nav = {'nav_dashboard': '', 'nav_runs': '', 'nav_logs': '', 'nav_report': '', 'nav_schema': ''}
|
||||
if active in nav:
|
||||
nav[active] = 'active'
|
||||
|
||||
@@ -58,6 +58,8 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
|
||||
nav_step_link = f'<a href="/steps" class="{cls}">RPA Steps</a>'
|
||||
cls_api_isc = 'active' if active == 'nav_api_iscrizioni' else ''
|
||||
nav_report_link = f'<a href="/iscrizioni-api" class="{cls_api_isc}">Iscrizioni</a>'
|
||||
cls_sp = 'active' if active == 'nav_sharepoint' else ''
|
||||
nav_sharepoint_link = f'<a href="/sharepoint" class="{cls_sp}">SharePoint</a>'
|
||||
cls_email = 'active' if active == 'nav_email' else ''
|
||||
nav_pec_link = f'<a href="/email" class="{cls_email}">Email</a>'
|
||||
else:
|
||||
@@ -67,6 +69,7 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
|
||||
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>'
|
||||
nav_sharepoint_link = ''
|
||||
|
||||
return _tmpl('_base.html').substitute(
|
||||
css=_css(), page_css=page_css, page_js=page_js, title=title, now=now,
|
||||
@@ -74,7 +77,8 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
|
||||
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_sharepoint_link=nav_sharepoint_link,
|
||||
nav_pec_link=nav_pec_link,
|
||||
**nav,
|
||||
)
|
||||
|
||||
+6
-1
@@ -8,7 +8,8 @@ from .config import _state, _access_log, DbType
|
||||
from .db_finder import _find_db, _pick_db_file
|
||||
from .pages import (
|
||||
_page_dashboard, _page_runs, _page_pec, _page_logs,
|
||||
_page_report, _page_documenti, _page_iscrizioni, _page_api_iscrizioni, _page_email, _page_server_logs,
|
||||
_page_report, _page_documenti, _page_iscrizioni, _page_api_iscrizioni, _page_sharepoint, _page_email, _page_server_logs,
|
||||
_page_schema,
|
||||
)
|
||||
from .queries.db import e as _e, query as _query, detect_db as _detect_db
|
||||
from .queries import documenti as q_documenti
|
||||
@@ -110,11 +111,13 @@ def make_handler(db_path: str):
|
||||
'/': lambda: _page_dashboard(db_path),
|
||||
'/runs': lambda: _page_runs(db_path),
|
||||
'/logs': _logs_page,
|
||||
'/schema': lambda: _page_schema(db_path),
|
||||
'/server-logs': lambda: _page_server_logs(db_path),
|
||||
}
|
||||
if db_type == DbType.INTRAZ:
|
||||
routes['/steps'] = lambda: _page_iscrizioni(db_path)
|
||||
routes['/iscrizioni-api'] = lambda: _page_api_iscrizioni(db_path)
|
||||
routes['/sharepoint'] = lambda: _page_sharepoint(db_path)
|
||||
routes['/email'] = lambda: _page_email(db_path)
|
||||
else:
|
||||
routes['/report'] = lambda: _page_report(db_path)
|
||||
@@ -146,11 +149,13 @@ def run_server(db_path: str, host: str = 'localhost', port: int = 8080):
|
||||
if db_type == DbType.INTRAZ:
|
||||
print(f" RPA Steps : http://localhost:{port}/steps")
|
||||
print(f" Iscr. API : http://localhost:{port}/iscrizioni-api")
|
||||
print(f" SharePoint : http://localhost:{port}/sharepoint")
|
||||
print(f" Email : http://localhost:{port}/email")
|
||||
else:
|
||||
print(f" PEC : http://localhost:{port}/pec")
|
||||
print(f" Documenti : http://localhost:{port}/documenti")
|
||||
print(f" Report : http://localhost:{port}/report")
|
||||
print(f" Schema : http://localhost:{port}/schema")
|
||||
print(f" Log DB : http://localhost:{port}/logs")
|
||||
print(f" Server log : http://localhost:{port}/server-logs")
|
||||
print(f" DB : {db_path}")
|
||||
|
||||
+13
-27
@@ -1,5 +1,17 @@
|
||||
(function () {
|
||||
|
||||
// ── Details toggle (show/hide data columns) ──────────────────────────────
|
||||
var btnDetails = document.getElementById('btn-toggle-details');
|
||||
var twMain = document.getElementById('tw-main');
|
||||
if (btnDetails && twMain) {
|
||||
btnDetails.classList.add('active');
|
||||
btnDetails.addEventListener('click', function () {
|
||||
var hidden = twMain.classList.toggle('data-hidden');
|
||||
btnDetails.classList.toggle('active', !hidden);
|
||||
requestAnimationFrame(syncMirrorWidth);
|
||||
});
|
||||
}
|
||||
|
||||
var COLS = ['sorgente', 'azienda', 'sessione', 'corso', 'data',
|
||||
'ftp', 'bc', 'iscrizione', 'odv', 'sharepoint', 'report', 'email'];
|
||||
var STEP_COLS = ['ftp', 'bc', 'iscrizione', 'odv', 'sharepoint', 'report', 'email'];
|
||||
@@ -205,32 +217,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── Sticky mirror scrollbar ───────────────────────────────────────────────
|
||||
(function () {
|
||||
var tw = document.getElementById('tw-main');
|
||||
var mirror = document.getElementById('tw-mirror');
|
||||
var inner = document.getElementById('tw-inner');
|
||||
if (!tw || !mirror || !inner) return;
|
||||
|
||||
function syncInnerWidth() {
|
||||
inner.style.width = tw.scrollWidth + 'px';
|
||||
}
|
||||
syncInnerWidth();
|
||||
new ResizeObserver(syncInnerWidth).observe(tw);
|
||||
|
||||
var syncing = false;
|
||||
mirror.addEventListener('scroll', function () {
|
||||
if (syncing) return;
|
||||
syncing = true;
|
||||
tw.scrollLeft = mirror.scrollLeft;
|
||||
syncing = false;
|
||||
});
|
||||
tw.addEventListener('scroll', function () {
|
||||
if (syncing) return;
|
||||
syncing = true;
|
||||
mirror.scrollLeft = tw.scrollLeft;
|
||||
syncing = false;
|
||||
});
|
||||
})();
|
||||
function syncMirrorWidth() {} // no-op, mirror replaced by native scrollbar
|
||||
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
(function () {
|
||||
if (typeof Chart === 'undefined' || !_schemaLabels || !_schemaLabels.length) return;
|
||||
|
||||
var COLORS = [
|
||||
'#2563eb','#16a34a','#d97706','#dc2626','#0284c7',
|
||||
'#6d28d9','#db2777','#059669','#ea580c','#0891b2',
|
||||
'#0f766e','#b45309','#7c3aed','#be185d','#0369a1'
|
||||
];
|
||||
|
||||
function palette(n) {
|
||||
var out = [];
|
||||
for (var i = 0; i < n; i++) out.push(COLORS[i % COLORS.length]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- bar chart: most used processes ---
|
||||
var ctxBar = document.getElementById('chartBar');
|
||||
if (ctxBar) {
|
||||
new Chart(ctxBar, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: _schemaLabels,
|
||||
datasets: [{
|
||||
label: 'Esecuzioni',
|
||||
data: _schemaCounts,
|
||||
backgroundColor: palette(_schemaLabels.length),
|
||||
borderRadius: 4,
|
||||
borderSkipped: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { precision: 0 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- pie chart: time per process ---
|
||||
var ctxPie = document.getElementById('chartPie');
|
||||
if (ctxPie) {
|
||||
new Chart(ctxPie, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: _schemaLabels,
|
||||
datasets: [{
|
||||
data: _schemaMinutes,
|
||||
backgroundColor: palette(_schemaLabels.length)
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'right', labels: { font: { size: 12 } } },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (ctx) {
|
||||
var min = ctx.parsed;
|
||||
var h = Math.floor(min / 60);
|
||||
var m = min % 60;
|
||||
var dur = h > 0 ? h + 'h ' + m + 'min' : m + 'min';
|
||||
return ' ' + ctx.label + ': ' + dur;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}());
|
||||
@@ -0,0 +1,16 @@
|
||||
(function(){
|
||||
function applyFilter() {
|
||||
var active = {};
|
||||
document.querySelectorAll('.email-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('.email-filter input[type="checkbox"]').forEach(function(chk) {
|
||||
chk.addEventListener('change', applyFilter);
|
||||
});
|
||||
})();
|
||||
+35
-20
@@ -145,28 +145,16 @@
|
||||
.cfp-all { font-weight: 600 !important; }
|
||||
.cfp-sep { height: 1px; background: var(--brd, #e2e8f0); margin: 3px 0; }
|
||||
|
||||
/* Horizontal scroll — let table grow beyond container */
|
||||
.tw { overflow-x: hidden; }
|
||||
/* Horizontal scroll with blue native scrollbar */
|
||||
.tw { overflow-x: auto; scrollbar-width: thin; scrollbar-color: #2563eb #dbeafe; }
|
||||
.tw::-webkit-scrollbar { height: 14px; }
|
||||
.tw::-webkit-scrollbar-track { background: #dbeafe; border-radius: 99px; }
|
||||
.tw::-webkit-scrollbar-thumb { background: #2563eb; border-radius: 99px; border: 2px solid #dbeafe; }
|
||||
.tw::-webkit-scrollbar-thumb:hover { background: #1d4ed8; }
|
||||
.tw table { width: max-content; min-width: 100%; }
|
||||
|
||||
/* Sticky mirror scrollbar — always visible at viewport bottom */
|
||||
.tw-scroll-mirror {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
height: 14px;
|
||||
background: var(--surf);
|
||||
border-top: 1px solid var(--brd);
|
||||
z-index: 40;
|
||||
}
|
||||
.tw-scroll-inner { height: 1px; }
|
||||
|
||||
.tw-scroll-mirror { scrollbar-color: #2563eb #dbeafe; scrollbar-width: thin; }
|
||||
.tw-scroll-mirror::-webkit-scrollbar { height: 14px; }
|
||||
.tw-scroll-mirror::-webkit-scrollbar-track { background: #dbeafe; border-radius: 99px; }
|
||||
.tw-scroll-mirror::-webkit-scrollbar-thumb { background: #2563eb; border-radius: 99px; border: 2px solid #dbeafe; }
|
||||
.tw-scroll-mirror::-webkit-scrollbar-thumb:hover { background: #1d4ed8; }
|
||||
/* Mirror kept for layout, hidden */
|
||||
.tw-scroll-mirror { display: none; }
|
||||
|
||||
/* Step cells with inline error */
|
||||
.step-td { vertical-align: top; min-width: 80px; }
|
||||
@@ -201,3 +189,30 @@
|
||||
background: #eff6ff;
|
||||
border-color: var(--pri, #2563eb);
|
||||
}
|
||||
|
||||
/* Details toggle button */
|
||||
.btn-toggle-details {
|
||||
background: none;
|
||||
border: 1px solid var(--brd, #e2e8f0);
|
||||
border-radius: 5px;
|
||||
padding: .28rem .75rem;
|
||||
font-size: .82rem;
|
||||
cursor: pointer;
|
||||
color: var(--mut, #64748b);
|
||||
font-family: inherit;
|
||||
transition: background .12s, border-color .12s, color .12s;
|
||||
}
|
||||
.btn-toggle-details.active {
|
||||
background: #eff6ff;
|
||||
border-color: var(--pri, #2563eb);
|
||||
color: var(--pri, #2563eb);
|
||||
}
|
||||
.btn-toggle-details:hover { border-color: var(--pri, #2563eb); color: var(--pri, #2563eb); }
|
||||
|
||||
/* Hide data columns when Details is off */
|
||||
.data-hidden .data-th,
|
||||
.data-hidden .data-td { display: none; }
|
||||
|
||||
/* Log link button inside # column */
|
||||
.td-id { white-space: nowrap; }
|
||||
.td-id .btn-log { margin-left: 5px; vertical-align: middle; }
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.schema-charts{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:8px}
|
||||
.chart-box{background:var(--surf);border:1px solid var(--brd);border-radius:8px;padding:20px 24px}
|
||||
.chart-box h3{font-size:.75rem;font-weight:600;color:var(--mut);margin-bottom:16px;
|
||||
text-transform:uppercase;letter-spacing:.05em}
|
||||
@media(max-width:800px){.schema-charts{grid-template-columns:1fr}}
|
||||
@@ -73,3 +73,6 @@ th.sort-desc::after{content:' \25BC';font-size:.65rem;opacity:.7}
|
||||
.modal-box li{background:var(--bg);border:1px solid var(--brd);border-radius:6px;padding:10px 14px;font-size:.875rem;font-weight:500}
|
||||
.modal-close{position:absolute;top:12px;right:14px;background:none;border:none;font-size:1.2rem;cursor:pointer;color:var(--mut);line-height:1;padding:2px 6px;border-radius:4px}
|
||||
.modal-close:hover{color:var(--txt);background:var(--bg)}
|
||||
/* Log link button (shared across pages) */
|
||||
.btn-log{display:inline-block;padding:1px 7px;border:1px solid var(--brd,#e2e8f0);border-radius:4px;font-size:.78rem;line-height:1.6;color:var(--pri,#2563eb);text-decoration:none;white-space:nowrap;transition:background .12s,border-color .12s}
|
||||
.btn-log:hover{background:#eff6ff;border-color:var(--pri,#2563eb)}
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
<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>
|
||||
<a href="/" class="hdr-nav ${nav_dashboard}">Dashboard</a>
|
||||
<a href="/schema" class="hdr-nav ${nav_schema}">Schema</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">
|
||||
@@ -30,6 +31,7 @@
|
||||
<nav>
|
||||
${nav_step_link}
|
||||
${nav_report_link}
|
||||
${nav_sharepoint_link}
|
||||
${nav_pec_link}
|
||||
</nav>
|
||||
<main>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="page-filter-wrap">
|
||||
<div class="isc-header">
|
||||
<h2>📋 Corsi Intraziendali</h2>
|
||||
<button id="btn-toggle-details" class="btn-toggle-details" title="Mostra/nascondi BC e Iscrizione">Details</button>
|
||||
<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>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<section class="page-header">
|
||||
<div class="page-filter-wrap">
|
||||
<div class="runs-header">
|
||||
<h2>Processi RPA (ultimi 50)</h2>
|
||||
<h2>Processi RPA</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>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<section>
|
||||
<h2>📊 Schema Processi</h2>
|
||||
<div class="schema-charts">
|
||||
<div class="chart-box">
|
||||
<h3>Processi più usati</h3>
|
||||
<canvas id="chartBar"></canvas>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<h3>Tempo per processo (minuti)</h3>
|
||||
<canvas id="chartPie"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
var _schemaLabels = ${labels_json};
|
||||
var _schemaCounts = ${counts_json};
|
||||
var _schemaMinutes = ${minutes_json};
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<section class="page-header">
|
||||
<div class="page-filter-wrap">
|
||||
<div class="email-header">
|
||||
<h2>SharePoint (ultimi 500)</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="email-filter">
|
||||
<label><input type="checkbox" data-stato="uploaded" checked> Uploaded</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_sharepoint}</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user